pptx-cli 1.1.0__tar.gz → 1.2.0__tar.gz
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.
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/.github/skills/pptx-deck-builder/SKILL.md +58 -56
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/.github/skills/pptx-deck-builder/references/excal-diagrams.md +2 -2
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/.github/skills/pptx-deck-builder/references/pptx-workflow.md +10 -1
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/PKG-INFO +6 -1
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/README.md +5 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/src/pptx_cli/__init__.py +1 -1
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/src/pptx_cli/core/template.py +203 -2
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/src/pptx_cli/models/manifest.py +11 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/tests/test_cli.py +27 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/.editorconfig +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/.github/copilot-instructions.md +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/.github/instructions/backend.instructions.md +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/.github/instructions/testing.instructions.md +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/.github/skills/pptx/SKILL.md +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/.github/skills/pptx/references/deck-spec.md +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/.github/skills/pptx-deck-builder/references/mckinsey-style.md +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/.github/workflows/ci.yml +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/.github/workflows/publish.yml +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/.gitignore +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/.python-version +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/AGENTS.md +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/ARCHITECTURE.md +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/CHANGELOG.md +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/CLI-MANIFEST.md +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/CONTRIBUTING.md +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/DECISIONS/ADR-0001-initial-architecture.md +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/LICENSE +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/PRD.md +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/PREVIEW-FUTURE.md +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/PROJECT.md +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/SCAFFOLD.md +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/SECURITY.md +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/TESTING.md +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/docs/DOMAIN.md +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/docs/GLOSSARY.md +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/docs/ROADMAP.md +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/docs/SCAFFOLDING-NOTES.md +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/guardrails/.gitignore +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/guardrails/guardrails.jsonl +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/guardrails/links.jsonl +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/guardrails/references.jsonl +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/guardrails/taxonomy.json +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/guardrails-explorer.html +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/pyproject.toml +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/pyrightconfig.json +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/scripts/bump_version.py +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/src/pptx_cli/__main__.py +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/src/pptx_cli/cli.py +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/src/pptx_cli/commands/__init__.py +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/src/pptx_cli/commands/compose.py +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/src/pptx_cli/commands/guide.py +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/src/pptx_cli/commands/init.py +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/src/pptx_cli/commands/inspect.py +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/src/pptx_cli/commands/manifest_ops.py +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/src/pptx_cli/commands/validate.py +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/src/pptx_cli/commands/wrapper.py +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/src/pptx_cli/core/__init__.py +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/src/pptx_cli/core/composition.py +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/src/pptx_cli/core/ids.py +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/src/pptx_cli/core/io.py +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/src/pptx_cli/core/manifest_store.py +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/src/pptx_cli/core/runtime.py +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/src/pptx_cli/core/validation.py +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/src/pptx_cli/core/versioning.py +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/src/pptx_cli/models/__init__.py +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/src/pptx_cli/models/envelope.py +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/tests/conftest.py +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/tests/test_versioning.py +0 -0
- {pptx_cli-1.1.0 → pptx_cli-1.2.0}/uv.lock +0 -0
|
@@ -66,10 +66,6 @@ Then run the doctor to verify compatibility:
|
|
|
66
66
|
pptx doctor --manifest ./corp-template --format json
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
**Important:** After init, picture-type placeholders are missing `image` in
|
|
70
|
-
their `supported_content_types`. Fix this before using images — see the
|
|
71
|
-
image handling section below.
|
|
72
|
-
|
|
73
69
|
### Step 2 — Discover layouts and placeholders
|
|
74
70
|
|
|
75
71
|
List available layouts, then inspect placeholders for the ones you plan to use:
|
|
@@ -87,7 +83,29 @@ placeholder:
|
|
|
87
83
|
- `supported_content_types` — What it accepts (`text`, `markdown-text`,
|
|
88
84
|
`image`, `table`, `chart`)
|
|
89
85
|
- `required` — Whether it must be filled
|
|
90
|
-
- `
|
|
86
|
+
- `estimated_text_capacity` — Preferred normalized guidance for text-capable
|
|
87
|
+
placeholders. Read `max_lines` first, then check `confidence`, `source`,
|
|
88
|
+
and `font_size_pt`.
|
|
89
|
+
- `text_defaults` — Raw extracted placeholder hints such as suggested font
|
|
90
|
+
size or explicit `max_lines` from the template text
|
|
91
|
+
- `left_emu`, `top_emu`, `width_emu`, `height_emu` — The actual geometry.
|
|
92
|
+
Treat `width_emu` and `height_emu` as hard constraints for how much content
|
|
93
|
+
the placeholder can realistically hold.
|
|
94
|
+
|
|
95
|
+
For slide drafting, prefer `estimated_text_capacity.max_lines` over parsing
|
|
96
|
+
`text_defaults.max_lines` directly. Treat it as guidance, not a hard rule:
|
|
97
|
+
stay at or below the line estimate when possible, and be more conservative
|
|
98
|
+
when `confidence` is `low`.
|
|
99
|
+
|
|
100
|
+
Respect placeholder size in every content decision:
|
|
101
|
+
|
|
102
|
+
- Text: do not write beyond the likely line budget for the box. If the message
|
|
103
|
+
does not fit, tighten the wording or choose a layout with a larger text area.
|
|
104
|
+
- Images and diagrams: match the placeholder aspect ratio and expected visual
|
|
105
|
+
density to the available width and height.
|
|
106
|
+
- Tables and charts: reduce rows, columns, labels, or series when the
|
|
107
|
+
placeholder is small. Prefer a larger layout over forcing dense content into
|
|
108
|
+
a small box.
|
|
91
109
|
|
|
92
110
|
Also inspect theme colors and assets if you need to match the visual identity:
|
|
93
111
|
|
|
@@ -112,11 +130,14 @@ letterboxing.
|
|
|
112
130
|
```bash
|
|
113
131
|
# Create the .excalidraw file, then:
|
|
114
132
|
excal validate diagrams/my-diagram.excalidraw
|
|
115
|
-
excal render diagrams/my-diagram.excalidraw --outDir ./out/diagrams --png --scale
|
|
133
|
+
excal render diagrams/my-diagram.excalidraw --outDir ./out/diagrams --png --scale 4 --no-background
|
|
116
134
|
```
|
|
117
135
|
|
|
118
136
|
Use `--no-background` for transparent backgrounds that blend with slide
|
|
119
|
-
backgrounds. Use `--scale
|
|
137
|
+
backgrounds. Use `--scale 4` for crisp output — scale 2 looks blurry on
|
|
138
|
+
large slide placeholders because the pixel density is too low for the
|
|
139
|
+
physical size. Scale 4 ensures diagrams stay sharp even on high-DPI
|
|
140
|
+
displays and when projected.
|
|
120
141
|
|
|
121
142
|
**Match the template's visual identity.** Diagrams should feel like they belong
|
|
122
143
|
on the slide, not like foreign objects pasted in. Two things matter most:
|
|
@@ -190,59 +211,29 @@ Use `--dry-run` on build to preview without writing files.
|
|
|
190
211
|
|
|
191
212
|
---
|
|
192
213
|
|
|
193
|
-
## Image handling
|
|
194
|
-
|
|
195
|
-
### Picture placeholders need `image` added to supported_content_types
|
|
196
|
-
|
|
197
|
-
After `pptx init`, picture-type placeholders only list `["text",
|
|
198
|
-
"markdown-text"]`. You must edit `manifest.yaml` to add `- image`:
|
|
214
|
+
## Image handling
|
|
199
215
|
|
|
200
|
-
|
|
201
|
-
# Find all placeholder_type: picture entries and add image:
|
|
202
|
-
placeholder_type: picture
|
|
203
|
-
guidance_lines: []
|
|
204
|
-
supported_content_types:
|
|
205
|
-
- text
|
|
206
|
-
- markdown-text
|
|
207
|
-
- image # ← add this line
|
|
208
|
-
```
|
|
216
|
+
### Image scaling — fit vs crop
|
|
209
217
|
|
|
210
|
-
|
|
218
|
+
Picture placeholders should already advertise `image` in
|
|
219
|
+
`supported_content_types`. Do not patch `manifest.yaml` unless inspection
|
|
220
|
+
output proves the template contract is actually wrong.
|
|
211
221
|
|
|
212
|
-
|
|
222
|
+
The current build behavior defaults to `image_fit: fit`, which preserves the
|
|
223
|
+
full image inside the placeholder. Use `image_fit: cover` when you explicitly
|
|
224
|
+
want crop-to-fill behavior.
|
|
213
225
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
pic = shape.insert_picture(str(image_path))
|
|
224
|
-
pic.crop_left = pic.crop_right = pic.crop_top = pic.crop_bottom = 0
|
|
225
|
-
img = pic.image
|
|
226
|
-
img_w, img_h = img.size
|
|
227
|
-
img_aspect = img_w / img_h
|
|
228
|
-
ph_aspect = ph_width / ph_height
|
|
229
|
-
if img_aspect > ph_aspect:
|
|
230
|
-
new_width = ph_width
|
|
231
|
-
new_height = int(ph_width / img_aspect)
|
|
232
|
-
else:
|
|
233
|
-
new_height = ph_height
|
|
234
|
-
new_width = int(ph_height * img_aspect)
|
|
235
|
-
pic.left = ph_left + (ph_width - new_width) // 2
|
|
236
|
-
pic.top = ph_top + (ph_height - new_height) // 2
|
|
237
|
-
pic.width = new_width
|
|
238
|
-
pic.height = new_height
|
|
226
|
+
```yaml
|
|
227
|
+
slides:
|
|
228
|
+
- layout: picture-layout-id
|
|
229
|
+
content:
|
|
230
|
+
title: Workflow
|
|
231
|
+
picture:
|
|
232
|
+
kind: image
|
|
233
|
+
path: out/diagrams/workflow.png
|
|
234
|
+
image_fit: fit
|
|
239
235
|
```
|
|
240
236
|
|
|
241
|
-
Check whether this patch is already applied before applying it. Read the
|
|
242
|
-
`_apply_content_value` function in `composition.py` — if it only has
|
|
243
|
-
`shape.insert_picture(str(image_path))` with no scaling logic, the patch
|
|
244
|
-
is needed.
|
|
245
|
-
|
|
246
237
|
---
|
|
247
238
|
|
|
248
239
|
## Slide writing principles
|
|
@@ -262,6 +253,17 @@ conclude — not just name the topic.
|
|
|
262
253
|
If a slide has two insights, split it into two slides. The body must prove
|
|
263
254
|
the headline.
|
|
264
255
|
|
|
256
|
+
### Respect placeholder geometry
|
|
257
|
+
|
|
258
|
+
Never treat placeholder size as flexible. The template geometry is part of the
|
|
259
|
+
contract.
|
|
260
|
+
|
|
261
|
+
- If text exceeds the placeholder's likely capacity, shorten it or split it.
|
|
262
|
+
- If a table or chart needs more room, switch to a layout with a larger object
|
|
263
|
+
placeholder.
|
|
264
|
+
- If a diagram becomes unreadable at the placeholder's size, simplify the
|
|
265
|
+
diagram rather than shrinking text and shapes until they are illegible.
|
|
266
|
+
|
|
265
267
|
### Source attribution
|
|
266
268
|
|
|
267
269
|
Data-driven slides need source lines. Use the `source` placeholder when
|
|
@@ -285,7 +287,7 @@ Match content to the right layout:
|
|
|
285
287
|
| Content type | Suggested layout patterns |
|
|
286
288
|
|---|---|
|
|
287
289
|
| Opening / title | front-page layouts (with pattern or picture) |
|
|
288
|
-
| Agenda / TOC | agenda layouts |
|
|
290
|
+
| Agenda / TOC | agenda layouts (use plain text items without numbered prefixes — the layout often auto-numbers) |
|
|
289
291
|
| Section divider | breaker layouts |
|
|
290
292
|
| Single topic with bullets | title-and-content |
|
|
291
293
|
| Side-by-side comparison | two-contents |
|
|
@@ -323,7 +325,7 @@ pptx validate --manifest ./manifest-dir --deck deck.pptx --strict --format json
|
|
|
323
325
|
|
|
324
326
|
# Diagrams
|
|
325
327
|
excal validate diagram.excalidraw
|
|
326
|
-
excal render diagram.excalidraw --outDir ./out --png --scale
|
|
328
|
+
excal render diagram.excalidraw --outDir ./out --png --scale 4 --no-background
|
|
327
329
|
|
|
328
330
|
# Template versioning
|
|
329
331
|
pptx manifest diff ./v1-manifest ./v2-manifest --format json
|
{pptx_cli-1.1.0 → pptx_cli-1.2.0}/.github/skills/pptx-deck-builder/references/excal-diagrams.md
RENAMED
|
@@ -18,7 +18,7 @@ excal validate diagram.excalidraw --check-assets
|
|
|
18
18
|
### excal render <file|->
|
|
19
19
|
Render to SVG, PNG, or PDF. PNG/PDF require Playwright.
|
|
20
20
|
```bash
|
|
21
|
-
excal render diagram.excalidraw --outDir ./out --png --scale
|
|
21
|
+
excal render diagram.excalidraw --outDir ./out --png --scale 4 --no-background
|
|
22
22
|
excal render diagram.excalidraw --outDir ./out --svg
|
|
23
23
|
excal render diagram.excalidraw --outDir ./out --png --dark-mode
|
|
24
24
|
excal render diagram.excalidraw --outDir ./out --png --frame "Frame Name"
|
|
@@ -31,7 +31,7 @@ Flags:
|
|
|
31
31
|
- `--pdf` — Export PDF (requires Playwright)
|
|
32
32
|
- `--dark-mode` — Dark theme
|
|
33
33
|
- `--no-background` — Transparent background
|
|
34
|
-
- `--scale <n>` — Scale factor for PNG (default: 2)
|
|
34
|
+
- `--scale <n>` — Scale factor for PNG (default: 2, use 4 for slide-quality output)
|
|
35
35
|
- `--padding <n>` — Padding in pixels (default: 20)
|
|
36
36
|
- `--frame <id|name>` — Export specific frame only
|
|
37
37
|
- `--element <id>` — Export specific element only
|
{pptx_cli-1.1.0 → pptx_cli-1.2.0}/.github/skills/pptx-deck-builder/references/pptx-workflow.md
RENAMED
|
@@ -58,9 +58,18 @@ Key fields per placeholder:
|
|
|
58
58
|
- `supported_content_types` — Array of: text, markdown-text, image, table, chart
|
|
59
59
|
- `required` — Boolean
|
|
60
60
|
- `overflow_policy` — fit, warn, or truncate
|
|
61
|
-
- `
|
|
61
|
+
- `estimated_text_capacity` — Preferred agent guidance with `max_lines`,
|
|
62
|
+
`confidence`, `source`, and inferred typography metadata
|
|
63
|
+
- `text_defaults` — Raw extracted placeholder hints with fields such as
|
|
64
|
+
`suggested_font_size_pt`, `max_lines`, and `alignment`
|
|
62
65
|
- `left_emu`, `top_emu`, `width_emu`, `height_emu` — Position/size in EMUs
|
|
63
66
|
|
|
67
|
+
Treat placeholder geometry as a hard layout constraint:
|
|
68
|
+
- Text should stay within the likely line budget for the box.
|
|
69
|
+
- Images and diagrams should match the placeholder aspect ratio.
|
|
70
|
+
- Tables and charts should be simplified or moved to a larger layout when the
|
|
71
|
+
placeholder is too small for legible output.
|
|
72
|
+
|
|
64
73
|
### pptx theme show --manifest <dir>
|
|
65
74
|
Show theme metadata: colors, fonts, effects.
|
|
66
75
|
```bash
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pptx-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Template-bound PowerPoint generation for enterprise decks
|
|
5
5
|
Author: Thomas Rohde
|
|
6
6
|
License: MIT License
|
|
@@ -81,6 +81,7 @@ The result is a CLI that behaves more like a compiler toolchain than a drawing t
|
|
|
81
81
|
|
|
82
82
|
- Initialize a manifest package from a real enterprise `.pptx`
|
|
83
83
|
- Inspect layouts, placeholders, themes, assets, and compatibility warnings
|
|
84
|
+
- Estimate text placeholder line capacity for agent guidance during inspection
|
|
84
85
|
- Build slides from approved layouts only
|
|
85
86
|
- Build full decks from JSON/YAML specs
|
|
86
87
|
- Preserve template-bound masters, themes, geometry, and protected elements
|
|
@@ -139,6 +140,10 @@ pptx layouts show title-only --manifest ./corp-template
|
|
|
139
140
|
pptx placeholders list 1-title-and-content --manifest ./corp-template
|
|
140
141
|
```
|
|
141
142
|
|
|
143
|
+
`pptx placeholders list --format json` includes both explicit placeholder guidance and
|
|
144
|
+
an `estimated_text_capacity` block for text-capable placeholders so agents can stay
|
|
145
|
+
inside likely line limits when drafting slide content.
|
|
146
|
+
|
|
142
147
|
Create a single slide:
|
|
143
148
|
|
|
144
149
|
```bash
|
|
@@ -35,6 +35,7 @@ The result is a CLI that behaves more like a compiler toolchain than a drawing t
|
|
|
35
35
|
|
|
36
36
|
- Initialize a manifest package from a real enterprise `.pptx`
|
|
37
37
|
- Inspect layouts, placeholders, themes, assets, and compatibility warnings
|
|
38
|
+
- Estimate text placeholder line capacity for agent guidance during inspection
|
|
38
39
|
- Build slides from approved layouts only
|
|
39
40
|
- Build full decks from JSON/YAML specs
|
|
40
41
|
- Preserve template-bound masters, themes, geometry, and protected elements
|
|
@@ -93,6 +94,10 @@ pptx layouts show title-only --manifest ./corp-template
|
|
|
93
94
|
pptx placeholders list 1-title-and-content --manifest ./corp-template
|
|
94
95
|
```
|
|
95
96
|
|
|
97
|
+
`pptx placeholders list --format json` includes both explicit placeholder guidance and
|
|
98
|
+
an `estimated_text_capacity` block for text-capable placeholders so agents can stay
|
|
99
|
+
inside likely line limits when drafting slide content.
|
|
100
|
+
|
|
96
101
|
Create a single slide:
|
|
97
102
|
|
|
98
103
|
```bash
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import hashlib
|
|
4
|
+
import math
|
|
4
5
|
import re
|
|
5
6
|
import shutil
|
|
6
7
|
import zipfile
|
|
@@ -29,13 +30,18 @@ from pptx_cli.models.manifest import (
|
|
|
29
30
|
PlaceholderContract,
|
|
30
31
|
ProtectedElement,
|
|
31
32
|
TemplateInfo,
|
|
33
|
+
TextCapacityGuidance,
|
|
32
34
|
ThemeModel,
|
|
33
35
|
)
|
|
34
36
|
|
|
35
37
|
_DRAWINGML_NS = {"a": "http://schemas.openxmlformats.org/drawingml/2006/main"}
|
|
38
|
+
_PRESENTATIONML_NS = {"p": "http://schemas.openxmlformats.org/presentationml/2006/main"}
|
|
39
|
+
_PPTX_NS = {**_DRAWINGML_NS, **_PRESENTATIONML_NS}
|
|
36
40
|
_PLACEHOLDER_TYPE_NAMES = {item.value: item.name.lower() for item in PP_PLACEHOLDER}
|
|
37
41
|
_IMAGE_SUFFIXES = {".png", ".jpg", ".jpeg", ".gif", ".bmp", ".svg", ".tif", ".tiff"}
|
|
38
42
|
_MEDIA_SUFFIXES = {".mp4", ".wmv", ".avi", ".mov", ".mp3", ".wav", ".m4v"}
|
|
43
|
+
_EMU_PER_POINT = 12700
|
|
44
|
+
_DEFAULT_LINE_HEIGHT_MULTIPLIER = 1.2
|
|
39
45
|
_NUMBER_WORDS = {
|
|
40
46
|
"one": 1,
|
|
41
47
|
"two": 2,
|
|
@@ -172,6 +178,188 @@ def _extract_text_defaults(shape: Any) -> dict[str, Any]:
|
|
|
172
178
|
return {key: value for key, value in defaults.items() if value not in (None, [], "")}
|
|
173
179
|
|
|
174
180
|
|
|
181
|
+
def _emu_to_points(value: int) -> float:
|
|
182
|
+
return round(value / _EMU_PER_POINT, 2)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _placeholder_text_style_bucket(placeholder_type: str) -> str:
|
|
186
|
+
if placeholder_type in {"title", "center_title", "vertical_title"}:
|
|
187
|
+
return "title"
|
|
188
|
+
if placeholder_type in {"body", "object", "content", "text", "subtitle"}:
|
|
189
|
+
return "body"
|
|
190
|
+
return "other"
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _extract_master_text_styles(master: Any) -> dict[str, dict[int, float]]:
|
|
194
|
+
styles: dict[str, dict[int, float]] = {}
|
|
195
|
+
for bucket, tag_name in {
|
|
196
|
+
"title": "titleStyle",
|
|
197
|
+
"body": "bodyStyle",
|
|
198
|
+
"other": "otherStyle",
|
|
199
|
+
}.items():
|
|
200
|
+
style_root = master.element.find(f".//p:{tag_name}", namespaces=_PPTX_NS)
|
|
201
|
+
if style_root is None:
|
|
202
|
+
styles[bucket] = {}
|
|
203
|
+
continue
|
|
204
|
+
|
|
205
|
+
level_sizes: dict[int, float] = {}
|
|
206
|
+
default_size_pt: float | None = None
|
|
207
|
+
default_paragraph = style_root.find("./a:defPPr", namespaces=_PPTX_NS)
|
|
208
|
+
if default_paragraph is not None:
|
|
209
|
+
default_run = default_paragraph.find("./a:defRPr", namespaces=_PPTX_NS)
|
|
210
|
+
default_size_pt = _font_size_from_xml(default_run)
|
|
211
|
+
|
|
212
|
+
for level in range(1, 10):
|
|
213
|
+
paragraph_style = style_root.find(f"./a:lvl{level}pPr", namespaces=_PPTX_NS)
|
|
214
|
+
if paragraph_style is None:
|
|
215
|
+
continue
|
|
216
|
+
default_run = paragraph_style.find("./a:defRPr", namespaces=_PPTX_NS)
|
|
217
|
+
font_size_pt = _font_size_from_xml(default_run)
|
|
218
|
+
if font_size_pt is not None:
|
|
219
|
+
level_sizes[level - 1] = font_size_pt
|
|
220
|
+
|
|
221
|
+
if default_size_pt is not None:
|
|
222
|
+
level_sizes.setdefault(0, default_size_pt)
|
|
223
|
+
styles[bucket] = level_sizes
|
|
224
|
+
|
|
225
|
+
return styles
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _font_size_from_xml(element: Any) -> float | None:
|
|
229
|
+
if element is None:
|
|
230
|
+
return None
|
|
231
|
+
size = element.get("sz")
|
|
232
|
+
if size is None:
|
|
233
|
+
return None
|
|
234
|
+
try:
|
|
235
|
+
return int(size) / 100
|
|
236
|
+
except ValueError:
|
|
237
|
+
return None
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _resolve_font_size_pt(
|
|
241
|
+
shape: Any,
|
|
242
|
+
placeholder_type: str,
|
|
243
|
+
text_defaults: dict[str, Any],
|
|
244
|
+
master_text_styles: dict[str, dict[int, float]],
|
|
245
|
+
) -> tuple[float | None, str | None]:
|
|
246
|
+
suggested_size = text_defaults.get("suggested_font_size_pt")
|
|
247
|
+
if suggested_size is not None:
|
|
248
|
+
return float(suggested_size), "guidance_text"
|
|
249
|
+
|
|
250
|
+
text_frame = getattr(shape, "text_frame", None)
|
|
251
|
+
if text_frame is not None:
|
|
252
|
+
for paragraph in text_frame.paragraphs:
|
|
253
|
+
if paragraph.font.size is not None:
|
|
254
|
+
return float(paragraph.font.size.pt), "paragraph_font"
|
|
255
|
+
for run in paragraph.runs:
|
|
256
|
+
if run.font.size is not None:
|
|
257
|
+
return float(run.font.size.pt), "run_font"
|
|
258
|
+
|
|
259
|
+
bucket = _placeholder_text_style_bucket(placeholder_type)
|
|
260
|
+
text_level = 0
|
|
261
|
+
if text_frame is not None and text_frame.paragraphs:
|
|
262
|
+
text_level = int(text_frame.paragraphs[0].level or 0)
|
|
263
|
+
bucket_styles = master_text_styles.get(bucket, {})
|
|
264
|
+
font_size_pt = bucket_styles.get(text_level)
|
|
265
|
+
if font_size_pt is not None:
|
|
266
|
+
return float(font_size_pt), "master_text_style"
|
|
267
|
+
if bucket != "other":
|
|
268
|
+
font_size_pt = master_text_styles.get("other", {}).get(text_level)
|
|
269
|
+
if font_size_pt is not None:
|
|
270
|
+
return float(font_size_pt), "master_text_style"
|
|
271
|
+
return None, None
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _resolve_font_family(
|
|
275
|
+
shape: Any,
|
|
276
|
+
placeholder_type: str,
|
|
277
|
+
text_defaults: dict[str, Any],
|
|
278
|
+
theme: ThemeModel,
|
|
279
|
+
) -> tuple[str | None, str | None]:
|
|
280
|
+
suggested_family = text_defaults.get("suggested_font_family")
|
|
281
|
+
if suggested_family is not None:
|
|
282
|
+
return str(suggested_family), "guidance_text"
|
|
283
|
+
|
|
284
|
+
text_frame = getattr(shape, "text_frame", None)
|
|
285
|
+
if text_frame is not None:
|
|
286
|
+
for paragraph in text_frame.paragraphs:
|
|
287
|
+
if paragraph.font.name:
|
|
288
|
+
return str(paragraph.font.name), "paragraph_font"
|
|
289
|
+
for run in paragraph.runs:
|
|
290
|
+
if run.font.name:
|
|
291
|
+
return str(run.font.name), "run_font"
|
|
292
|
+
|
|
293
|
+
bucket = _placeholder_text_style_bucket(placeholder_type)
|
|
294
|
+
if bucket == "title":
|
|
295
|
+
theme_family = theme.fonts.get("major") or theme.fonts.get("major_latin")
|
|
296
|
+
else:
|
|
297
|
+
theme_family = theme.fonts.get("minor") or theme.fonts.get("minor_latin")
|
|
298
|
+
if theme_family is None:
|
|
299
|
+
theme_family = theme.fonts.get("major") or theme.fonts.get("minor")
|
|
300
|
+
if theme_family is None:
|
|
301
|
+
return None, None
|
|
302
|
+
return theme_family, "theme"
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _estimate_text_capacity(
|
|
306
|
+
shape: Any,
|
|
307
|
+
placeholder_type: str,
|
|
308
|
+
supported_content_types: list[str],
|
|
309
|
+
text_defaults: dict[str, Any],
|
|
310
|
+
master_text_styles: dict[str, dict[int, float]],
|
|
311
|
+
theme: ThemeModel,
|
|
312
|
+
) -> TextCapacityGuidance | None:
|
|
313
|
+
if "text" not in supported_content_types and "markdown-text" not in supported_content_types:
|
|
314
|
+
return None
|
|
315
|
+
|
|
316
|
+
text_frame = getattr(shape, "text_frame", None)
|
|
317
|
+
if text_frame is None:
|
|
318
|
+
return None
|
|
319
|
+
|
|
320
|
+
font_size_pt, font_size_source = _resolve_font_size_pt(
|
|
321
|
+
shape,
|
|
322
|
+
placeholder_type,
|
|
323
|
+
text_defaults,
|
|
324
|
+
master_text_styles,
|
|
325
|
+
)
|
|
326
|
+
if font_size_pt is None:
|
|
327
|
+
return None
|
|
328
|
+
|
|
329
|
+
font_family, _ = _resolve_font_family(shape, placeholder_type, text_defaults, theme)
|
|
330
|
+
margin_top = int(text_frame.margin_top or 0)
|
|
331
|
+
margin_bottom = int(text_frame.margin_bottom or 0)
|
|
332
|
+
usable_height_emu = max(int(shape.height) - margin_top - margin_bottom, 0)
|
|
333
|
+
usable_height_pt = _emu_to_points(usable_height_emu)
|
|
334
|
+
if usable_height_pt <= 0:
|
|
335
|
+
return None
|
|
336
|
+
|
|
337
|
+
line_height_pt = round(font_size_pt * _DEFAULT_LINE_HEIGHT_MULTIPLIER, 2)
|
|
338
|
+
explicit_max_lines = text_defaults.get("max_lines")
|
|
339
|
+
if explicit_max_lines is not None:
|
|
340
|
+
return TextCapacityGuidance(
|
|
341
|
+
max_lines=int(explicit_max_lines),
|
|
342
|
+
source="explicit_guidance",
|
|
343
|
+
confidence="high",
|
|
344
|
+
font_size_pt=round(font_size_pt, 2),
|
|
345
|
+
font_family=font_family,
|
|
346
|
+
usable_height_pt=usable_height_pt,
|
|
347
|
+
line_height_pt=line_height_pt,
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
inferred_lines = max(1, math.floor(usable_height_pt / line_height_pt))
|
|
351
|
+
confidence = "medium" if font_size_source is not None else "low"
|
|
352
|
+
return TextCapacityGuidance(
|
|
353
|
+
max_lines=inferred_lines,
|
|
354
|
+
source="inferred",
|
|
355
|
+
confidence=confidence,
|
|
356
|
+
font_size_pt=round(font_size_pt, 2),
|
|
357
|
+
font_family=font_family,
|
|
358
|
+
usable_height_pt=usable_height_pt,
|
|
359
|
+
line_height_pt=line_height_pt,
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
|
|
175
363
|
def _extract_theme(zip_file: zipfile.ZipFile) -> ThemeModel:
|
|
176
364
|
theme_candidates = [
|
|
177
365
|
name
|
|
@@ -237,6 +425,7 @@ def _is_protected_placeholder(shape_name: str) -> bool:
|
|
|
237
425
|
|
|
238
426
|
def _build_layouts(
|
|
239
427
|
prs: Any,
|
|
428
|
+
theme: ThemeModel,
|
|
240
429
|
) -> tuple[list[MasterContract], list[LayoutContract], list[LayoutAnnotation]]:
|
|
241
430
|
master_ids: list[MasterContract] = []
|
|
242
431
|
layouts: list[LayoutContract] = []
|
|
@@ -244,9 +433,11 @@ def _build_layouts(
|
|
|
244
433
|
layout_ids_seen: set[str] = set()
|
|
245
434
|
|
|
246
435
|
master_id_map: dict[int, str] = {}
|
|
436
|
+
master_style_map: dict[int, dict[str, dict[int, float]]] = {}
|
|
247
437
|
for master_index, master in enumerate(prs.slide_masters):
|
|
248
438
|
master_id = f"master-{master_index + 1}"
|
|
249
439
|
master_id_map[id(master)] = master_id
|
|
440
|
+
master_style_map[id(master)] = _extract_master_text_styles(master)
|
|
250
441
|
master_ids.append(
|
|
251
442
|
MasterContract(
|
|
252
443
|
id=master_id,
|
|
@@ -286,6 +477,15 @@ def _build_layouts(
|
|
|
286
477
|
logical_names_seen,
|
|
287
478
|
)
|
|
288
479
|
supported_content_types = _supports_content_types(placeholder_type, shape.name)
|
|
480
|
+
text_defaults = _extract_text_defaults(shape)
|
|
481
|
+
estimated_text_capacity = _estimate_text_capacity(
|
|
482
|
+
shape,
|
|
483
|
+
placeholder_type,
|
|
484
|
+
supported_content_types,
|
|
485
|
+
text_defaults,
|
|
486
|
+
master_style_map.get(id(layout.slide_master), {}),
|
|
487
|
+
theme,
|
|
488
|
+
)
|
|
289
489
|
placeholders.append(
|
|
290
490
|
PlaceholderContract(
|
|
291
491
|
logical_name=logical_name,
|
|
@@ -300,7 +500,8 @@ def _build_layouts(
|
|
|
300
500
|
width_emu=int(shape.width),
|
|
301
501
|
height_emu=int(shape.height),
|
|
302
502
|
required=logical_name == "title",
|
|
303
|
-
text_defaults=
|
|
503
|
+
text_defaults=text_defaults,
|
|
504
|
+
estimated_text_capacity=estimated_text_capacity,
|
|
304
505
|
inheritance_chain=[master_id, layout_id],
|
|
305
506
|
)
|
|
306
507
|
)
|
|
@@ -511,7 +712,7 @@ def build_manifest_package(
|
|
|
511
712
|
prs = Presentation(str(template))
|
|
512
713
|
with zipfile.ZipFile(template) as zip_file:
|
|
513
714
|
theme = _extract_theme(zip_file)
|
|
514
|
-
masters, layouts, annotations = _build_layouts(prs)
|
|
715
|
+
masters, layouts, annotations = _build_layouts(prs, theme)
|
|
515
716
|
assets = _copy_template_and_assets(template, output_dir)
|
|
516
717
|
findings = _compatibility_findings(template)
|
|
517
718
|
has_errors = any(item.severity == "error" for item in findings)
|
|
@@ -40,6 +40,16 @@ class ProtectedElement(BaseModel):
|
|
|
40
40
|
fingerprint: str
|
|
41
41
|
|
|
42
42
|
|
|
43
|
+
class TextCapacityGuidance(BaseModel):
|
|
44
|
+
max_lines: int
|
|
45
|
+
source: Literal["explicit_guidance", "inferred"] = "inferred"
|
|
46
|
+
confidence: Literal["high", "medium", "low"] = "medium"
|
|
47
|
+
font_size_pt: float | None = None
|
|
48
|
+
font_family: str | None = None
|
|
49
|
+
usable_height_pt: float | None = None
|
|
50
|
+
line_height_pt: float | None = None
|
|
51
|
+
|
|
52
|
+
|
|
43
53
|
class PlaceholderContract(BaseModel):
|
|
44
54
|
logical_name: str
|
|
45
55
|
source_name: str
|
|
@@ -55,6 +65,7 @@ class PlaceholderContract(BaseModel):
|
|
|
55
65
|
required: bool = False
|
|
56
66
|
overflow_policy: Literal["fit", "warn", "truncate"] = "warn"
|
|
57
67
|
text_defaults: dict[str, Any] = Field(default_factory=dict)
|
|
68
|
+
estimated_text_capacity: TextCapacityGuidance | None = None
|
|
58
69
|
inheritance_chain: list[str] = Field(default_factory=list)
|
|
59
70
|
allowed_formatting_overrides: list[str] = Field(default_factory=list)
|
|
60
71
|
|
|
@@ -164,6 +164,29 @@ def test_inspection_commands_use_manifest_contract(
|
|
|
164
164
|
assert title_placeholder["text_defaults"]["max_lines"] == 2
|
|
165
165
|
assert title_placeholder["text_defaults"]["suggested_font_size_pt"] == 24.0
|
|
166
166
|
assert title_placeholder["text_defaults"]["suggested_font_family"] == "DB Regular"
|
|
167
|
+
assert title_placeholder["estimated_text_capacity"]["max_lines"] == 2
|
|
168
|
+
assert title_placeholder["estimated_text_capacity"]["source"] == "explicit_guidance"
|
|
169
|
+
assert title_placeholder["estimated_text_capacity"]["confidence"] == "high"
|
|
170
|
+
assert title_placeholder["estimated_text_capacity"]["font_size_pt"] == 24.0
|
|
171
|
+
|
|
172
|
+
subtitle_placeholder = next(
|
|
173
|
+
placeholder for placeholder in placeholders if placeholder["logical_name"] == "subtitle"
|
|
174
|
+
)
|
|
175
|
+
assert subtitle_placeholder["estimated_text_capacity"]["max_lines"] == 1
|
|
176
|
+
assert subtitle_placeholder["estimated_text_capacity"]["source"] == "inferred"
|
|
177
|
+
assert subtitle_placeholder["estimated_text_capacity"]["confidence"] == "medium"
|
|
178
|
+
|
|
179
|
+
content_layout_payload = _invoke_json(
|
|
180
|
+
["placeholders", "list", "1-title-and-content", "--manifest", str(manifest_dir)]
|
|
181
|
+
)
|
|
182
|
+
content_placeholder = next(
|
|
183
|
+
placeholder
|
|
184
|
+
for placeholder in content_layout_payload["result"]["placeholders"]
|
|
185
|
+
if placeholder["logical_name"] == "content_1"
|
|
186
|
+
)
|
|
187
|
+
assert content_placeholder["estimated_text_capacity"]["max_lines"] == 20
|
|
188
|
+
assert content_placeholder["estimated_text_capacity"]["source"] == "inferred"
|
|
189
|
+
assert content_placeholder["estimated_text_capacity"]["font_size_pt"] == 14.0
|
|
167
190
|
|
|
168
191
|
theme_payload = _invoke_json(["theme", "show", "--manifest", str(manifest_dir)])
|
|
169
192
|
assert "fonts" in theme_payload["result"]
|
|
@@ -567,6 +590,10 @@ def test_deck_build_validate_schema_and_diff(
|
|
|
567
590
|
schema_payload = _invoke_json(["manifest", "schema"])
|
|
568
591
|
assert "template" in schema_payload["result"]["properties"]
|
|
569
592
|
assert "$defs" in schema_payload["result"]
|
|
593
|
+
assert (
|
|
594
|
+
"estimated_text_capacity"
|
|
595
|
+
in schema_payload["result"]["$defs"]["PlaceholderContract"]["properties"]
|
|
596
|
+
)
|
|
570
597
|
|
|
571
598
|
diff_payload = _invoke_json(["manifest", "diff", str(manifest_dir), str(manifest_dir)])
|
|
572
599
|
assert diff_payload["result"]["breaking_changes"] == []
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pptx_cli-1.1.0 → pptx_cli-1.2.0}/.github/skills/pptx-deck-builder/references/mckinsey-style.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|