dsh-office-kit 0.1.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/LICENSE.md +21 -21
- package/README.md +108 -106
- 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 +146 -149
- package/postinstall.mjs +92 -82
- package/python/office_cli.py +171 -246
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""Compare a finished deck against the template it claims to follow.
|
|
2
|
+
|
|
3
|
+
Purpose: "it looks similar" is not fidelity. This gate takes explicit
|
|
4
|
+
requirements — which source slides must be represented, how close the slide
|
|
5
|
+
size and placeholder geometry must be, whether a photographic background must
|
|
6
|
+
survive — and answers yes or no. No requirements supplied means nothing to
|
|
7
|
+
enforce, so the gate passes neutrally instead of inventing a standard.
|
|
8
|
+
|
|
9
|
+
Requirements file:
|
|
10
|
+
{
|
|
11
|
+
"reference_slide_indices": [1, 3, 4],
|
|
12
|
+
"mapping": {"1": 1, "2": 3, "3": 4},
|
|
13
|
+
"minimum_coverage_ratio": 1.0,
|
|
14
|
+
"require_exact_dimensions": true,
|
|
15
|
+
"require_placeholder_geometry": true,
|
|
16
|
+
"require_photographic_background": false,
|
|
17
|
+
"geometry_tolerance_emu": 9144
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
Exit codes: 0 pass, 1 fail, 2 unreadable input.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
import sys
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
30
|
+
|
|
31
|
+
from ooxml_helpers import box, is_picture, slide_size_in # noqa: E402
|
|
32
|
+
|
|
33
|
+
EMU_PER_INCH = 914400
|
|
34
|
+
DEFAULT_TOLERANCE_EMU = 9144 # 0.01 in
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _placeholder_map(slide) -> dict[tuple[int, str], tuple[float, float, float, float]]:
|
|
38
|
+
entries: dict[tuple[int, str], tuple[float, float, float, float]] = {}
|
|
39
|
+
for shape in slide.shapes:
|
|
40
|
+
if not shape.is_placeholder:
|
|
41
|
+
continue
|
|
42
|
+
geometry = box(shape)
|
|
43
|
+
if geometry is None:
|
|
44
|
+
continue
|
|
45
|
+
entries[(shape.placeholder_format.idx, shape.name)] = geometry
|
|
46
|
+
return entries
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _background_picture(slide, slide_w: float, slide_h: float) -> bool:
|
|
50
|
+
for shape in slide.shapes:
|
|
51
|
+
if not is_picture(shape):
|
|
52
|
+
continue
|
|
53
|
+
geometry = box(shape)
|
|
54
|
+
if geometry is None:
|
|
55
|
+
continue
|
|
56
|
+
left, top, width, height = geometry
|
|
57
|
+
if left <= 0.02 and top <= 0.02 and width >= slide_w * 0.98 and height >= slide_h * 0.98:
|
|
58
|
+
return True
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def check_fidelity(template_path: str | Path, final_path: str | Path, requirements: dict | None) -> dict:
|
|
63
|
+
from pptx import Presentation
|
|
64
|
+
|
|
65
|
+
requirements = requirements or {}
|
|
66
|
+
template_file = Path(template_path)
|
|
67
|
+
final_file = Path(final_path)
|
|
68
|
+
for candidate in (template_file, final_file):
|
|
69
|
+
if not candidate.is_file():
|
|
70
|
+
return {"gate": "template-source-fidelity", "passed": False, "findings": [{"kind": "missing_file", "detail": str(candidate)}]}
|
|
71
|
+
|
|
72
|
+
if not requirements:
|
|
73
|
+
return {
|
|
74
|
+
"gate": "template-source-fidelity",
|
|
75
|
+
"passed": True,
|
|
76
|
+
"neutral": True,
|
|
77
|
+
"note": "no fidelity requirements were supplied, so nothing was enforced",
|
|
78
|
+
"findings": [],
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
template = Presentation(str(template_file))
|
|
82
|
+
final = Presentation(str(final_file))
|
|
83
|
+
template_w, template_h = slide_size_in(template)
|
|
84
|
+
final_w, final_h = slide_size_in(final)
|
|
85
|
+
tolerance_in = float(requirements.get("geometry_tolerance_emu", DEFAULT_TOLERANCE_EMU)) / EMU_PER_INCH
|
|
86
|
+
findings: list[dict] = []
|
|
87
|
+
checks: list[dict] = []
|
|
88
|
+
|
|
89
|
+
if requirements.get("require_exact_dimensions", True):
|
|
90
|
+
same = abs(template_w - final_w) <= tolerance_in and abs(template_h - final_h) <= tolerance_in
|
|
91
|
+
checks.append({"check": "dimensions", "passed": same, "template": [template_w, template_h], "final": [final_w, final_h]})
|
|
92
|
+
if not same:
|
|
93
|
+
findings.append(
|
|
94
|
+
{
|
|
95
|
+
"kind": "dimension_mismatch",
|
|
96
|
+
"severity": "fail",
|
|
97
|
+
"message": f"final deck is {final_w:.2f}x{final_h:.2f} in, template is {template_w:.2f}x{template_h:.2f} in",
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
template_slides = list(template.slides)
|
|
102
|
+
final_slides = list(final.slides)
|
|
103
|
+
mapping = requirements.get("mapping") or {}
|
|
104
|
+
pairs: list[tuple[int, int]] = []
|
|
105
|
+
for output_index, source_index in mapping.items():
|
|
106
|
+
pairs.append((int(output_index), int(source_index)))
|
|
107
|
+
if not pairs:
|
|
108
|
+
pairs = [(index, index) for index in range(1, min(len(template_slides), len(final_slides)) + 1)]
|
|
109
|
+
|
|
110
|
+
matched = 0
|
|
111
|
+
for output_index, source_index in pairs:
|
|
112
|
+
if not (1 <= output_index <= len(final_slides) and 1 <= source_index <= len(template_slides)):
|
|
113
|
+
findings.append(
|
|
114
|
+
{
|
|
115
|
+
"kind": "mapping_out_of_range",
|
|
116
|
+
"severity": "fail",
|
|
117
|
+
"message": f"mapping {output_index}->{source_index} is outside the deck",
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
continue
|
|
121
|
+
source_slide = template_slides[source_index - 1]
|
|
122
|
+
output_slide = final_slides[output_index - 1]
|
|
123
|
+
slide_pass = True
|
|
124
|
+
|
|
125
|
+
if source_slide.slide_layout.name != output_slide.slide_layout.name:
|
|
126
|
+
slide_pass = False
|
|
127
|
+
findings.append(
|
|
128
|
+
{
|
|
129
|
+
"kind": "layout_changed",
|
|
130
|
+
"severity": "fail",
|
|
131
|
+
"slide": output_index,
|
|
132
|
+
"sourceSlide": source_index,
|
|
133
|
+
"message": f"layout went from '{source_slide.slide_layout.name}' to '{output_slide.slide_layout.name}'",
|
|
134
|
+
}
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
if requirements.get("require_placeholder_geometry", True):
|
|
138
|
+
source_placeholders = _placeholder_map(source_slide)
|
|
139
|
+
output_placeholders = _placeholder_map(output_slide)
|
|
140
|
+
for key, source_box in source_placeholders.items():
|
|
141
|
+
output_box = output_placeholders.get(key)
|
|
142
|
+
if output_box is None:
|
|
143
|
+
continue
|
|
144
|
+
drift = max(abs(a - b) for a, b in zip(source_box, output_box))
|
|
145
|
+
if drift > tolerance_in:
|
|
146
|
+
slide_pass = False
|
|
147
|
+
findings.append(
|
|
148
|
+
{
|
|
149
|
+
"kind": "placeholder_geometry_drift",
|
|
150
|
+
"severity": "fail",
|
|
151
|
+
"slide": output_index,
|
|
152
|
+
"placeholder": key[1],
|
|
153
|
+
"driftIn": round(drift, 3),
|
|
154
|
+
"message": f"placeholder '{key[1]}' moved {drift:.2f} in from the template position",
|
|
155
|
+
}
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
if requirements.get("require_photographic_background"):
|
|
159
|
+
if _background_picture(source_slide, template_w, template_h) and not _background_picture(output_slide, final_w, final_h):
|
|
160
|
+
slide_pass = False
|
|
161
|
+
findings.append(
|
|
162
|
+
{
|
|
163
|
+
"kind": "background_lost",
|
|
164
|
+
"severity": "fail",
|
|
165
|
+
"slide": output_index,
|
|
166
|
+
"sourceSlide": source_index,
|
|
167
|
+
"message": "the template slide carries a full-bleed background image; the output does not",
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
checks.append({"check": "slide", "outputSlide": output_index, "sourceSlide": source_index, "passed": slide_pass})
|
|
172
|
+
matched += 1 if slide_pass else 0
|
|
173
|
+
|
|
174
|
+
coverage = matched / len(pairs) if pairs else 0.0
|
|
175
|
+
minimum = float(requirements.get("minimum_coverage_ratio", 1.0))
|
|
176
|
+
checks.append({"check": "coverage", "ratio": round(coverage, 4), "minimum": minimum, "passed": coverage + 1e-9 >= minimum})
|
|
177
|
+
if coverage + 1e-9 < minimum:
|
|
178
|
+
findings.append(
|
|
179
|
+
{
|
|
180
|
+
"kind": "coverage_below_minimum",
|
|
181
|
+
"severity": "fail",
|
|
182
|
+
"message": f"{coverage:.0%} of mapped slides preserved the template, minimum is {minimum:.0%}",
|
|
183
|
+
}
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
reference = requirements.get("reference_slide_indices") or []
|
|
187
|
+
if reference and not pairs:
|
|
188
|
+
findings.append({"kind": "no_usable_mapping", "severity": "fail", "message": f"reference slides {reference} could not be mapped to output slides"})
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
"gate": "template-source-fidelity",
|
|
192
|
+
"template": str(template_file.resolve()),
|
|
193
|
+
"final": str(final_file.resolve()),
|
|
194
|
+
"requirements": requirements,
|
|
195
|
+
"checks": checks,
|
|
196
|
+
"findings": findings,
|
|
197
|
+
"coverage": round(coverage, 4),
|
|
198
|
+
"passed": not [item for item in findings if item["severity"] == "fail"],
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def main(argv: list[str] | None = None) -> int:
|
|
203
|
+
import argparse
|
|
204
|
+
|
|
205
|
+
parser = argparse.ArgumentParser(description="Verify that a deck preserved the template it was built from.")
|
|
206
|
+
parser.add_argument("--template", required=True, help="the source template .pptx")
|
|
207
|
+
parser.add_argument("--final", required=True, help="the finished .pptx")
|
|
208
|
+
parser.add_argument("--requirements", help="JSON file with the fidelity requirements; omit for a neutral pass")
|
|
209
|
+
parser.add_argument("--json-out", help="also write the report here")
|
|
210
|
+
args = parser.parse_args(argv)
|
|
211
|
+
|
|
212
|
+
requirements = None
|
|
213
|
+
if args.requirements:
|
|
214
|
+
try:
|
|
215
|
+
requirements = json.loads(Path(args.requirements).read_text(encoding="utf-8"))
|
|
216
|
+
except Exception as exc: # noqa: BLE001
|
|
217
|
+
json.dump({"gate": "template-source-fidelity", "passed": False, "error": {"code": "bad_requirements", "message": str(exc)}}, sys.stdout)
|
|
218
|
+
sys.stdout.write("\n")
|
|
219
|
+
return 2
|
|
220
|
+
report = check_fidelity(args.template, args.final, requirements)
|
|
221
|
+
if args.json_out:
|
|
222
|
+
Path(args.json_out).write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
223
|
+
json.dump(report, sys.stdout, ensure_ascii=False, indent=2)
|
|
224
|
+
sys.stdout.write("\n")
|
|
225
|
+
return 0 if report["passed"] else 1
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
if __name__ == "__main__":
|
|
229
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Finance decks
|
|
2
|
+
|
|
3
|
+
The mechanics are the same as any deck; the failure modes are specific.
|
|
4
|
+
|
|
5
|
+
## What a finance audience checks first
|
|
6
|
+
|
|
7
|
+
1. Does the total add up? A slide whose numbers do not sum loses the argument
|
|
8
|
+
before the argument starts. Run the arithmetic gate on every declared total.
|
|
9
|
+
2. Is the period stated? "Revenue up 12%" without a base period is not a claim.
|
|
10
|
+
3. Is the unit stated? Thousands, millions, currency, and whether it is gross or
|
|
11
|
+
net. Put it in the header, once.
|
|
12
|
+
4. Is the comparison like-for-like? Full quarter against full quarter; do not
|
|
13
|
+
put a 13-week quarter beside a 12-week one without saying so.
|
|
14
|
+
5. Does the chart axis start at zero? For bars, yes, always — a truncated bar
|
|
15
|
+
chart in a finance deck reads as manipulation even when it is not.
|
|
16
|
+
|
|
17
|
+
## The financial slide kit
|
|
18
|
+
|
|
19
|
+
| Slide | Content | Note |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| Headline | One number, one claim, one source | The number the meeting is about |
|
|
22
|
+
| Bridge | Waterfall from last period to this one | Native chart; label the middle bars, not the ends |
|
|
23
|
+
| Results table | Period rows, metric columns, total row | Totals computed, not typed |
|
|
24
|
+
| Variance | Actual vs plan vs prior, and why | The "why" is a sentence, not a column |
|
|
25
|
+
| Outlook | Ranges, not points | A single forecast point implies precision you do not have |
|
|
26
|
+
| Ask | The decision, the amount, the date | Last slide, one line |
|
|
27
|
+
|
|
28
|
+
## Numbers on slides
|
|
29
|
+
|
|
30
|
+
- Two significant figures is usually enough for a headline (`£2.4m`, not
|
|
31
|
+
`£2,384,117`). Keep full precision in the appendix table.
|
|
32
|
+
- Percentages: one decimal at most, and say what they are a percentage *of*.
|
|
33
|
+
- Round consistently: if the headline says 2.4m, the table should not say
|
|
34
|
+
2.38m in one row and 2.4m in the next.
|
|
35
|
+
- Negative numbers: use a minus sign consistently (`−12%`) and keep
|
|
36
|
+
parentheses for tables where accounting convention is expected.
|
|
37
|
+
- A variance is meaningless without its sign and its direction; state whether
|
|
38
|
+
favourable is up or down (revenue vs cost).
|
|
39
|
+
|
|
40
|
+
## Charts that survive a CFO
|
|
41
|
+
|
|
42
|
+
- Waterfall for a bridge, column for periods, line for a trend, never pie for
|
|
43
|
+
more than three slices.
|
|
44
|
+
- Label the axis unit once, in the caption: "£m, FY2026 actuals".
|
|
45
|
+
- One series per chart unless the comparison *is* the point.
|
|
46
|
+
- Do not chart a table of six numbers; the table is more honest and faster to
|
|
47
|
+
read.
|
|
48
|
+
- Forecast periods are visually distinct (lighter fill, dashed line) and named
|
|
49
|
+
in the caption.
|
|
50
|
+
|
|
51
|
+
## Provenance
|
|
52
|
+
|
|
53
|
+
Every non-obvious number gets a note with: source file or system, extraction
|
|
54
|
+
date, period covered, and any adjustment made. The speaker notes are the right
|
|
55
|
+
place; a footnote-heavy slide is a slide nobody reads.
|
|
56
|
+
|
|
57
|
+
## Check before delivery
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
"<python>" "<skill>/container_tools/native_table_arithmetic_gate.py" --input deck.pptx --contracts totals.json
|
|
61
|
+
"<python>" "<skill>/container_tools/native_quantitative_chart_gate.py" --input deck.pptx --all-slides
|
|
62
|
+
"<python>" "<skill>/container_tools/inspect_presentation_layout_geometry.py" --input deck.pptx
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Then read the slides as a sceptic: which number would you challenge first, and
|
|
66
|
+
does the deck already answer it?
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Slide mechanics and paste-runnable recipes
|
|
2
|
+
|
|
3
|
+
The helper CLI covers the common path: a small deck with a title, bullet slides,
|
|
4
|
+
a table, a chart and a picture. Everything else — precise coordinates, master
|
|
5
|
+
edits, notes, theme tokens, connectors — is ordinary `python-pptx` work, and
|
|
6
|
+
these recipes are the honest route.
|
|
7
|
+
|
|
8
|
+
Read the mechanics first, then paste the recipe. Every snippet runs as-is from a
|
|
9
|
+
shell, from any working directory:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
"<python>" - <<'PY'
|
|
13
|
+
from pptx import Presentation
|
|
14
|
+
...
|
|
15
|
+
PY
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## The four numbers that decide every layout
|
|
19
|
+
|
|
20
|
+
| Question | Answer |
|
|
21
|
+
|---|---|
|
|
22
|
+
| Where is this shape? | `left`, `top`, `width`, `height` in **EMU** (English Metric Units). |
|
|
23
|
+
| What is an inch? | `914400` EMU. `pptx.util.Inches(1)` is the polite wrapper. |
|
|
24
|
+
| What is a point? | `12700` EMU. Font sizes use points, geometry uses EMU, and mixing them silently breaks layout. |
|
|
25
|
+
| What is a slide? | `prs.slide_width` / `prs.slide_height`; check them, never assume 16:9. |
|
|
26
|
+
|
|
27
|
+
Helper conversions worth memorising: `Emu(914400) == Inches(1)`,
|
|
28
|
+
`Pt(28) == Emu(355600)`, and a fraction of the slide is
|
|
29
|
+
`int(0.1 * prs.slide_width)`.
|
|
30
|
+
|
|
31
|
+
## Z-order
|
|
32
|
+
|
|
33
|
+
Shapes paint in the order they live in `slide.shapes`. A shape added later
|
|
34
|
+
covers one added earlier. To reorder without deleting:
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
tree = slide.shapes._spTree
|
|
38
|
+
shape = slide.shapes[3]
|
|
39
|
+
tree.remove(shape._element)
|
|
40
|
+
tree.insert(2, shape._element) # after nvGrpSpPr and grpSpPr
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Index `2` is the first painted shape; that is where a full-bleed background
|
|
44
|
+
image belongs.
|
|
45
|
+
|
|
46
|
+
## What this pack ships
|
|
47
|
+
|
|
48
|
+
| Recipe | Use it when |
|
|
49
|
+
|---|---|
|
|
50
|
+
| [textbox-at-coordinate](textbox-at-coordinate.md) | Text must land on an exact rect, not "somewhere under the title". |
|
|
51
|
+
| [autolayout](autolayout.md) | The same build has to fit 3 bullets or 9 without hand-tuning. |
|
|
52
|
+
| [tables](tables.md) | Numbers need a grid with column widths and per-cell formatting. |
|
|
53
|
+
| [native-charts](native-charts.md) | Data must stay editable, not become a picture of a chart. |
|
|
54
|
+
| [master-and-layouts](master-and-layouts.md) | The whole deck changes at once: title bar, accent line, footer. |
|
|
55
|
+
| [speaker-notes](speaker-notes.md) | The presenter needs a script attached to each slide. |
|
|
56
|
+
| [theme-tokens](theme-tokens.md) | Colours and fonts must come from one place. |
|
|
57
|
+
| [rich-text-runs](rich-text-runs.md) | One paragraph mixes bold, accent colour and a footnote. |
|
|
58
|
+
| [images](images.md) | A picture needs a crop, a cover fit or a real alt text. |
|
|
59
|
+
| [connectors](connectors.md) | Two boxes are joined by a line that must survive a move. |
|
|
60
|
+
|
|
61
|
+
## Order of work that keeps decks out of trouble
|
|
62
|
+
|
|
63
|
+
1. Read the target geometry off the template or the user's reference
|
|
64
|
+
(`container_tools/inspect_presentation_layout_geometry.py` prints it as JSON).
|
|
65
|
+
2. Place shapes in EMU, top-down: background, chrome, title, body, evidence.
|
|
66
|
+
3. Set type sizes by the *smallest* screen the deck will be shown on; body 18 pt
|
|
67
|
+
and up, footnotes 12 pt and up.
|
|
68
|
+
4. Fill every placeholder you keep and delete the ones you do not — an empty
|
|
69
|
+
placeholder prints "Click to add text" in edit view and looks unfinished.
|
|
70
|
+
5. Render, look, then fix. `container_tools/render_slides.py` plus `read_image`
|
|
71
|
+
is the loop; a deck nobody looked at is not verified.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Auto-layout rules
|
|
2
|
+
|
|
3
|
+
## Mechanics
|
|
4
|
+
|
|
5
|
+
PowerPoint has two different "autofit" ideas and mixing them up wastes a round
|
|
6
|
+
trip:
|
|
7
|
+
|
|
8
|
+
- `word_wrap` decides whether text wraps inside the width. Turn it on for every
|
|
9
|
+
authored box.
|
|
10
|
+
- `auto_size` decides whether the *frame* follows the text (`MSO_AUTO_SIZE.
|
|
11
|
+
SHAPE_TO_FIT_TEXT`) or the text shrinks to fit the frame
|
|
12
|
+
(`TEXT_TO_FIT_SHAPE`). `TEXT_TO_FIT_SHAPE` is a **render-time** behaviour:
|
|
13
|
+
PowerPoint recalculates it when the file is opened, so a deck that looks fine
|
|
14
|
+
in a draft preview can still shrink to 8 pt on a real screen.
|
|
15
|
+
|
|
16
|
+
The reliable pattern is: fixed frame, explicit sizes, content measured against
|
|
17
|
+
the frame, no text-to-fit. Estimate, then verify by rendering.
|
|
18
|
+
|
|
19
|
+
## Recipe: fit a variable number of bullets
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
"<python>" - <<'PY'
|
|
23
|
+
from pptx import Presentation
|
|
24
|
+
from pptx.util import Inches, Pt
|
|
25
|
+
from pptx.enum.text import MSO_ANCHOR
|
|
26
|
+
|
|
27
|
+
BULLETS = [
|
|
28
|
+
"Picking is 41% of floor labour hours",
|
|
29
|
+
"Travel between aisles adds 12 minutes per order",
|
|
30
|
+
"Manual cycle counts stop the line twice a week",
|
|
31
|
+
"Two cells idle below 60% utilisation",
|
|
32
|
+
"Overtime covers the Friday peak",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
def fits(text: str, width_in: float, height_in: float, size_pt: float) -> bool:
|
|
36
|
+
per_line = max(1, int(width_in * 96 / (0.55 * size_pt)))
|
|
37
|
+
lines = max(1, -(-len(text) // per_line))
|
|
38
|
+
return lines * 1.2 * size_pt / 72 <= height_in
|
|
39
|
+
|
|
40
|
+
prs = Presentation()
|
|
41
|
+
prs.slide_width, prs.slide_height = Inches(13.333), Inches(7.5)
|
|
42
|
+
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|
43
|
+
|
|
44
|
+
width, height = 8.4, 4.6 # the body rect you reserved
|
|
45
|
+
box = slide.shapes.add_textbox(Inches(0.9), Inches(1.6), Inches(width), Inches(height))
|
|
46
|
+
frame = box.text_frame
|
|
47
|
+
frame.word_wrap = True
|
|
48
|
+
frame.vertical_anchor = MSO_ANCHOR.TOP
|
|
49
|
+
|
|
50
|
+
size = next(pt for pt in (24, 22, 20, 18) if fits(" ".join(BULLETS), width, height, pt))
|
|
51
|
+
needed = sum(
|
|
52
|
+
max(1, -(-len(item) // max(1, int(width * 96 / (0.55 * size))))) * 1.2 * size / 72
|
|
53
|
+
for item in BULLETS
|
|
54
|
+
)
|
|
55
|
+
for index, item in enumerate(BULLETS):
|
|
56
|
+
paragraph = frame.paragraphs[0] if index == 0 else frame.add_paragraph()
|
|
57
|
+
paragraph.text = item
|
|
58
|
+
paragraph.font.size = Pt(size)
|
|
59
|
+
paragraph.space_after = Pt(10)
|
|
60
|
+
|
|
61
|
+
prs.save("autofit.pptx")
|
|
62
|
+
print(f"size={size}pt lines_estimate_in={needed:.2f} frame_in={height}")
|
|
63
|
+
PY
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Rules that hold regardless of content
|
|
67
|
+
|
|
68
|
+
1. Body text is 18 pt or larger; below that the back row cannot read it.
|
|
69
|
+
2. Six bullets or three lines of body is the point where a slide should split.
|
|
70
|
+
3. Longer text shrinks the number of *words*, not the type size. If the fit loop
|
|
71
|
+
lands below the body floor, cut copy or split the slide.
|
|
72
|
+
4. Keep 0.5 in of bottom margin for the footer and for projectors that crop.
|
|
73
|
+
5. Two columns beat one narrow column: a 4.2 in column of 18 pt text is a wall.
|
|
74
|
+
|
|
75
|
+
## Pitfalls this avoids
|
|
76
|
+
|
|
77
|
+
- `frame.text` then `paragraph.font.size` on paragraph 0 only: a multi-paragraph
|
|
78
|
+
assignment leaves paragraphs 1..n at the inherited 18 pt.
|
|
79
|
+
- Text-to-fit hiding the overflow: the file opens fine and the audience reads
|
|
80
|
+
9 pt. Measure, do not delegate the fit to the renderer.
|
|
81
|
+
- A frame height that exactly equals the text height: one extra word on edit and
|
|
82
|
+
the block runs into the evidence below. Reserve 10% slack.
|
|
83
|
+
|
|
84
|
+
## CLI fast path
|
|
85
|
+
|
|
86
|
+
`pptx create` with `layout: "bullets"` fills the layout's body placeholder at
|
|
87
|
+
its own size. Use the loop above when the number of bullets is not known before
|
|
88
|
+
the content is written.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Connectors
|
|
2
|
+
|
|
3
|
+
## Mechanics
|
|
4
|
+
|
|
5
|
+
A connector is a line with two attachment sites. `add_connector` takes a begin
|
|
6
|
+
and end shape *plus* connection indices, which is what makes the line follow the
|
|
7
|
+
boxes when they move. If you pass coordinates instead, the line is a floating
|
|
8
|
+
line: it looks right in the file you made and wrong the first time a box is
|
|
9
|
+
nudged.
|
|
10
|
+
|
|
11
|
+
- `MSO_CONNECTOR.STRAIGHT | ELBOW | CURVE`
|
|
12
|
+
- `connector.begin_connect(shape, idx)` / `end_connect(shape, idx)`; `idx` is
|
|
13
|
+
the site index on the shape's outline (0 = top, 1 = left, 2 = bottom,
|
|
14
|
+
3 = right for a rectangle).
|
|
15
|
+
- Arrowheads are line properties: `line.end_arrowhead = MSO_ARROWHEAD.TRIANGLE`.
|
|
16
|
+
|
|
17
|
+
## Recipe
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
"<python>" - <<'PY'
|
|
21
|
+
from pptx import Presentation
|
|
22
|
+
from pptx.util import Inches, Pt
|
|
23
|
+
from pptx.enum.shapes import MSO_SHAPE, MSO_CONNECTOR
|
|
24
|
+
from pptx.dml.color import RGBColor
|
|
25
|
+
from pptx.enum.dml import MSO_LINE_DASH_STYLE
|
|
26
|
+
|
|
27
|
+
prs = Presentation()
|
|
28
|
+
prs.slide_width, prs.slide_height = Inches(13.333), Inches(7.5)
|
|
29
|
+
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|
30
|
+
|
|
31
|
+
def box(left, top, text, width=2.6, height=1.1):
|
|
32
|
+
shape = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, Inches(left), Inches(top), Inches(width), Inches(height))
|
|
33
|
+
shape.text_frame.text = text
|
|
34
|
+
shape.text_frame.paragraphs[0].font.size = Pt(14)
|
|
35
|
+
shape.fill.solid()
|
|
36
|
+
shape.fill.fore_color.rgb = RGBColor(0xF2, 0xF5, 0xF9)
|
|
37
|
+
shape.line.color.rgb = RGBColor(0xC8, 0xD2, 0xDF)
|
|
38
|
+
return shape
|
|
39
|
+
|
|
40
|
+
order = box(0.9, 2.2, "Order booked")
|
|
41
|
+
pick = box(4.4, 2.2, "Picked")
|
|
42
|
+
ship = box(7.9, 2.2, "Shipped")
|
|
43
|
+
|
|
44
|
+
for start, end in ((order, pick), (pick, ship)):
|
|
45
|
+
connector = slide.shapes.add_connector(
|
|
46
|
+
MSO_CONNECTOR.STRAIGHT, 0, 0, Inches(0.1), Inches(0.1)
|
|
47
|
+
)
|
|
48
|
+
connector.begin_connect(start, 3) # right edge
|
|
49
|
+
connector.end_connect(end, 1) # left edge
|
|
50
|
+
connector.line.width = Pt(2)
|
|
51
|
+
connector.line.color.rgb = RGBColor(0x5F, 0x6B, 0x7A)
|
|
52
|
+
connector.line.end_arrowhead = 3 # triangle
|
|
53
|
+
|
|
54
|
+
prs.save("connectors.pptx")
|
|
55
|
+
from pptx.oxml.ns import qn
|
|
56
|
+
print("connectors:", sum(1 for s in slide.shapes if s._element.tag == qn("p:cxnSp")))
|
|
57
|
+
PY
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Rules
|
|
61
|
+
|
|
62
|
+
1. Connect sides that face each other: right → left for a row, bottom → top for
|
|
63
|
+
a column. A line crossing its own boxes reads as an error.
|
|
64
|
+
2. Elbow connectors for orthogonal flows, straight for a single hop, curve almost
|
|
65
|
+
never.
|
|
66
|
+
3. Two crossing connectors in one diagram is the point where the diagram becomes
|
|
67
|
+
a picture of a diagram; split it.
|
|
68
|
+
4. Label the connector ("12 min", "auto") with a small textbox near its middle,
|
|
69
|
+
not with arrow direction alone.
|
|
70
|
+
|
|
71
|
+
## Pitfalls this avoids
|
|
72
|
+
|
|
73
|
+
- `add_connector` with free coordinates: the line is positioned but unattached,
|
|
74
|
+
and the deck's own edits break it silently.
|
|
75
|
+
- Arrowhead index confusion: arrowhead values are a bitmask enum, not a boolean.
|
|
76
|
+
- Connectors behind filled shapes because they were added before the boxes.
|
|
77
|
+
|
|
78
|
+
## CLI fast path
|
|
79
|
+
|
|
80
|
+
There is no connector verb in `pptx create`; this recipe is the route. After
|
|
81
|
+
building, `container_tools/inspect_presentation_layout_geometry.py` flags
|
|
82
|
+
`connector_over_text` when a line crosses a text shape, which is the failure
|
|
83
|
+
mode you cannot see in a thumbnail.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Images
|
|
2
|
+
|
|
3
|
+
## Mechanics
|
|
4
|
+
|
|
5
|
+
`add_picture(path, left, top, width=…, height=…)`:
|
|
6
|
+
|
|
7
|
+
- Give **width only** and the height follows the aspect ratio. Give both and
|
|
8
|
+
the picture is stretched — a deliberate choice for a full-bleed background,
|
|
9
|
+
a mistake for a product photo.
|
|
10
|
+
- `picture.crop_left/right/top/bottom` are fractions of the *source*; they crop
|
|
11
|
+
without changing the frame, which is how a "cover" fit is done.
|
|
12
|
+
- The bytes live in `ppt/media/`; the same file added twice is stored once,
|
|
13
|
+
keyed by content hash.
|
|
14
|
+
- `picture._element.nvPicPr.cNvPr.set("descr", "…")` is the alt text. Decks get
|
|
15
|
+
read aloud and exported; an untagged picture is an empty sentence.
|
|
16
|
+
|
|
17
|
+
## Recipe: cover-fit into a fixed frame
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
"<python>" - <<'PY'
|
|
21
|
+
from pptx import Presentation
|
|
22
|
+
from pptx.util import Inches
|
|
23
|
+
from PIL import Image
|
|
24
|
+
|
|
25
|
+
def cover(picture, frame_w_in, frame_h_in, source_w, source_h):
|
|
26
|
+
"""Crop the picture so it fills the frame without distortion."""
|
|
27
|
+
frame_ratio = frame_w_in / frame_h_in
|
|
28
|
+
source_ratio = source_w / source_h
|
|
29
|
+
if source_ratio > frame_ratio: # too wide: crop the sides
|
|
30
|
+
keep = frame_ratio / source_ratio
|
|
31
|
+
picture.crop_left = picture.crop_right = (1 - keep) / 2
|
|
32
|
+
elif source_ratio < frame_ratio: # too tall: crop top and bottom
|
|
33
|
+
keep = source_ratio / frame_ratio
|
|
34
|
+
picture.crop_top = picture.crop_bottom = (1 - keep) / 2
|
|
35
|
+
|
|
36
|
+
prs = Presentation()
|
|
37
|
+
prs.slide_width, prs.slide_height = Inches(13.333), Inches(7.5)
|
|
38
|
+
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|
39
|
+
|
|
40
|
+
with Image.open("aisle.png") as image:
|
|
41
|
+
source_w, source_h = image.size
|
|
42
|
+
|
|
43
|
+
frame = slide.shapes.add_picture("aisle.png", Inches(0.9), Inches(1.7), Inches(8.0), Inches(4.5))
|
|
44
|
+
cover(frame, 8.0, 4.5, source_w, source_h)
|
|
45
|
+
frame._element.nvPicPr.cNvPr.set("descr", "Racking aisle in the pilot cell at peak hour")
|
|
46
|
+
|
|
47
|
+
# full-bleed background, sent to the back
|
|
48
|
+
background = slide.shapes.add_picture("aisle.png", 0, 0, prs.slide_width, prs.slide_height)
|
|
49
|
+
tree = slide.shapes._spTree
|
|
50
|
+
tree.remove(background._element)
|
|
51
|
+
tree.insert(2, background._element)
|
|
52
|
+
prs.save("image.pptx")
|
|
53
|
+
print("placed", source_w, "x", source_h)
|
|
54
|
+
PY
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Rules
|
|
58
|
+
|
|
59
|
+
1. Never upscale past the source resolution: a 900 px wide image at 8 in is
|
|
60
|
+
~110 ppi and will look soft on a projector. Check `Image.open(...).size`.
|
|
61
|
+
2. One dominant image per slide. A slide with a hero and three thumbnails is two
|
|
62
|
+
slides.
|
|
63
|
+
3. Full-bleed backgrounds need text contrast handled deliberately — a dark
|
|
64
|
+
scrim rectangle between picture and text, not hope.
|
|
65
|
+
4. Alt text on every picture, in the language of the deck.
|
|
66
|
+
|
|
67
|
+
## Pitfalls this avoids
|
|
68
|
+
|
|
69
|
+
- Passing both width and height and getting a squashed photograph nobody can
|
|
70
|
+
name as the cause.
|
|
71
|
+
- `add_picture` after the text: the picture covers the words. Add backgrounds
|
|
72
|
+
first, or move them to index 2.
|
|
73
|
+
- Re-using a screenshot of a chart or a table as "evidence": it is unreadable at
|
|
74
|
+
projector size and unverifiable. Use a native chart or table.
|
|
75
|
+
|
|
76
|
+
## CLI fast path
|
|
77
|
+
|
|
78
|
+
`pptx create` accepts `{"image": {"path": "...", "left": 0.9, "top": 1.6,
|
|
79
|
+
"width": 8.0}}` and derives the height from the aspect ratio — no crop, no alt
|
|
80
|
+
text. Cropping, alt text and background placement are the recipe's job.
|