python-pptx2 3.1.0__py3-none-any.whl → 3.2.0__py3-none-any.whl

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.
@@ -1,495 +1,550 @@
1
- # Slide design: making a generated deck look designed
2
-
3
- `space-aware-authoring.md` makes a deck *physically* correct — nothing
4
- overflows, nothing falls off the slide. This file is about the other
5
- half: a deck that looks like a designer laid it out. Everything here is
6
- built from the public API (`BBox`, `add_text`, `add_card`, `add_bullets`,
7
- `add_picture_fit`, `PALETTES`, `pptx2.diagrams`, `render_contact_sheet`),
8
- so the code samples run as written.
9
-
10
- Read this when the deck will be *seen* — a lesson, a talk, a pitch — and
11
- not only parsed.
12
-
13
- ## The one-paragraph brief
14
-
15
- A slide that looks designed has **one idea**, **one focal point**, **one
16
- palette**, **one type family**, generous **air** around everything, and
17
- text that **fits at a readable size**. The viewer knows within a second
18
- where to look first. Every element on the slide is *content* (a title, a
19
- claim, a list, a picture, a table, a diagram, an equation) or *structure*
20
- (a surface, a rule, a caption); there is nothing whose only job is to
21
- decorate. When the content is strong the slide needs nothing else, and
22
- when the content is thin, ornament will not save it.
23
-
24
- ## 1. Start from the content
25
-
26
- Before drawing anything, write down for each slide:
27
-
28
- - **Purpose** — one sentence: what the viewer should know or feel after it.
29
- - **Carrier** — the single element that delivers the purpose: a
30
- statement, a short list, a picture, a table, a process, an equation.
31
- - **Support** — at most two more elements that help the carrier (a
32
- caption, a source, a question, a picture beside a list).
33
- - **Archetype** — pick one from the catalog in §10. Reusing a handful of
34
- archetypes is what makes a deck feel consistent.
35
-
36
- A deck of 8–12 slides typically needs 4–5 distinct archetypes. Alternate
37
- dense and light slides: after a table or a three-card slide, give the
38
- audience a statement or a full-bleed picture.
39
-
40
- ## 2. Canvas, margins, grid
41
-
42
- Work in 16:9 at 13.333 × 7.5 in, and set it explicitly:
43
-
44
- ```python
45
- from pptx2 import Presentation, BBox
46
- from pptx2.util import Inches
47
-
48
- prs = Presentation()
49
- prs.slide_width, prs.slide_height = Inches(13.333), Inches(7.5)
50
- W, H = 13.333, 7.5
51
- BLANK = prs.slide_layouts[6]
52
- ```
53
-
54
- Then let two regions govern every content slide:
55
-
56
- ```python
57
- TITLE = BBox.from_inches(0.8, 0.55, W - 1.6, 0.9) # title band
58
- BODY = BBox.from_inches(0.8, 1.7, W - 1.6, H - 2.4) # content area (ends at 6.8")
59
- ```
60
-
61
- - **Margins**: 0.8 in left/right, 0.55 in top, 0.7 in bottom. Nothing but
62
- a full-bleed picture or a coloured background touches the slide edge.
63
- - **Gutters**: 0.4–0.6 in between columns and cards. Use one value per deck.
64
- - **Columns**: `BODY.columns(3, gap=Inches(0.5))`,
65
- `BODY.split_h([7, 5], gap=Inches(0.6))`, `BODY.rows(2, gap=...)`. Every
66
- card, column and picture on a slide starts at the same left edge as the
67
- title, or at a grid line derived from it.
68
- - **Optical center**: single statements and hero titles sit slightly
69
- above the geometric middle (`anchor="middle"` in a box whose top is
70
- ~1.6 in and bottom ~5.4 in).
71
-
72
- ## 3. Typography
73
-
74
- One family for the whole deck. Noto Sans (or whatever the environment's
75
- default sans is) is a fine choice; the design comes from *scale* and
76
- *spacing*, not from the font.
77
-
78
- | Role | Size | Weight | Notes |
79
- |---|---|---|---|
80
- | Hero title (title / section slide) | 44–54 pt | bold | 1–2 lines |
81
- | Statement (the one-sentence slide) | 36–44 pt | bold | ≤ 20 words, centred or left |
82
- | Slide title | 30–36 pt | bold | one line; sentence case |
83
- | Body / bullets | 20–24 pt | regular | ≤ 6 items, ≤ 12 words each |
84
- | Card title | 20–24 pt | bold | |
85
- | Card body | 16–18 pt | regular | 2–4 lines |
86
- | Table cells | 16–20 pt | header bold | |
87
- | Caption / source / footer | 12–14 pt | regular or italic, `muted` | |
88
-
89
- Rules that make type look intentional:
90
-
91
- - **Sentence case** for titles ("Co rostlina potřebuje", not "CO ROSTLINA
92
- POTŘEBUJE"). Uppercase belongs to acronyms.
93
- - **Left-align running text.** Centre only short statements, titles on
94
- hero slides, and labels inside diagram nodes.
95
- - **Breathing room**: `line_spacing = 1.1–1.2` for body, `space_after =
96
- 8–10 pt` between bullets (`add_bullets(gap_pt=8)` does this).
97
- - **Zero text-box margins** when the box is already placed by the grid
98
- (`margin_pt=0`) so the text edge *is* the grid line.
99
- - **Hierarchy by size and weight, not by colour.** Reserve colour for the
100
- one thing on the slide that should pop (§4).
101
- - **Fit, then check**: `add_bullets` and `add_card` shrink text to the box
102
- and never below 12 pt. If they had to shrink, there is too much text —
103
- split the slide.
104
-
105
- ```python
106
- slide.shapes.add_text(TITLE, text="Co rostlina potřebuje",
107
- size_pt=32, bold=True, color=P.ink,
108
- anchor="middle", margin_pt=0)
109
- ```
110
-
111
- ## 4. Colour
112
-
113
- Pick **one** palette and use only its roles:
114
-
115
- ```python
116
- from pptx2 import PALETTES
117
-
118
- P = PALETTES["slate"] # slate · linen · forest · plum · ember · navy · graphite
119
- D = P.dark() # same hues on a dark paper — title, section, closing slides
120
- ```
121
-
122
- | Role | Use |
123
- |---|---|
124
- | `P.paper` | slide background (`slide.background.fill.solid(); ...fore_color.rgb = P.paper`) |
125
- | `P.ink` | all headings and body text |
126
- | `P.muted` | captions, sources, secondary labels, table grid text |
127
- | `P.surface` | card fills, table zebra rows, picture frames |
128
- | `P.line` | hairline rules and table borders |
129
- | `P.accent` | **one** emphasis per slide: the key number, the active step, a highlighted word, the header row |
130
- | `P.accent_soft` | fill of a callout card / highlighted row; text on it stays `P.ink` |
131
- | `P.accent_ink` | text placed *on* `P.accent` (white on a dark accent) |
132
-
133
- - **One accent per slide.** Two accents split attention; three look like a
134
- toy. Cards in a row all share `P.surface`; the one to emphasise (if any)
135
- gets `P.accent_soft`.
136
- - **Dark slides** for the title, section dividers and the closing slide
137
- give the deck rhythm. Use `D = P.dark()` so the hues stay in the family.
138
- - **Contrast**: every text/background pair ≥ 4.5:1 (the linter reports
139
- `LowContrast`); the shipped palettes satisfy this for `ink` on `paper`,
140
- `surface` and `accent_soft`, for `muted` on `paper`, and for `accent_ink`
141
- on `accent`.
142
- - **Photos bring their own colour.** Beside a photo, keep the rest of the
143
- slide to `paper` / `ink` / `muted`.
144
-
145
- ## 5. Surfaces: cards, callouts, panels
146
-
147
- A card is one tinted rounded rectangle with padded text inside it, and
148
- that is the whole card. Its silhouette stays a clean rectangle from every
149
- angle; emphasis comes from the tint (or, when the tint is unwanted, a
150
- hairline outline — one or the other, never both). Padding of 18–28 pt on
151
- every side gives the text room to sit rather than press against the edge.
152
-
153
- ```python
154
- from pptx2 import add_card
155
-
156
- for cell, (title, body) in zip(BODY.columns(3, gap=Inches(0.5)), items):
157
- add_card(slide, cell, title=title, body=body,
158
- fill=P.surface, title_color=P.ink, body_color=P.ink,
159
- title_size_pt=22, body_size_pt=18, pad_pt=22)
160
- ```
161
-
162
- Composition rules for groups of cards:
163
-
164
- - **2–4 cards per row**, equal size, equal gap, same fill, same radius.
165
- Five or more become a table.
166
- - **Same content shape in every card**: if one has a title and two lines,
167
- all have a title and two lines. Trim copy to match.
168
- - **A callout** (question, key definition, "remember this") is one wide
169
- card in `P.accent_soft`, inset from the body region so it reads as a
170
- deliberate object, not as a background.
171
- - **Cards hold content, backgrounds hold cards.** A card sits directly on
172
- the paper; it does not sit on another panel.
173
- - **Numbering** belongs *in* the title text ("1 · Zachycení"), where it
174
- aligns with everything else, rather than in a separate badge shape.
175
-
176
- `add_card` returns `card.inner` (the padded box) so a picture, equation
177
- or diagram can be placed inside the same surface:
178
-
179
- ```python
180
- c = add_card(slide, right_col, fill=P.surface)
181
- slide.shapes.add_equation(c.inner, latex=r"6\,CO_2 + 6\,H_2O \rightarrow C_6H_{12}O_6 + 6\,O_2", size_pt=28)
182
- ```
183
-
184
- ## 6. Whitespace and alignment
185
-
186
- - 30–40 % of a content slide is empty. That is the design working.
187
- - **One left edge.** Title, bullets, cards and pictures share the `0.8 in`
188
- margin (or a grid line derived from it). Right edges align to the
189
- mirror margin.
190
- - **Consistent vertical rhythm.** Title band → 0.25 in gap → body. Every
191
- content slide places its body at the same `BODY.top`, so flipping
192
- through the deck feels steady.
193
- - **Balance, not symmetry.** A picture on the right with a list on the
194
- left is balanced when the list's text block is roughly as tall as the
195
- picture. Use `split_h([7, 5])` for text-heavy pairs, `[1, 1]` for a
196
- true comparison.
197
- - **Empty is fine, unbalanced is not.** A slide whose right half is blank
198
- because the list is short: make the list larger (24 pt), add a picture,
199
- or narrow the text column and centre the pair.
200
-
201
- ## 7. Pictures
202
-
203
- Photos carry emotion and specificity — one good photo does more than three
204
- small ones.
205
-
206
- ```python
207
- from pptx2 import add_picture_fit
208
-
209
- # Beside text: fit inside the column, never distort, caption in muted.
210
- add_picture_fit(slide, "leaf.jpg", right_col, mode="contain",
211
- caption="Buk lesní · Wikimedia Commons", caption_color=P.muted)
212
-
213
- # Full-bleed hero: crop to fill the whole slide, then a dark band for text.
214
- with slide.design_group("hero"): # photo + band + title are one deliberate stack
215
- add_picture_fit(slide, "forest.jpg", BBox.from_inches(0, 0, W, H), mode="cover")
216
- band = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, *BBox.from_inches(0, H - 2.2, W, 2.2))
217
- band.fill_hex(D.paper); band.line.fill.background(); band.shadow.clear()
218
- band.fill.fore_color.alpha = 0.8 # translucent, photo shows through
219
- slide.shapes.add_text(BBox.from_inches(0.8, H - 1.9, W - 1.6, 1.6),
220
- text="Les jako plíce planety", size_pt=44, bold=True,
221
- color=D.ink, anchor="middle", margin_pt=0)
222
- ```
223
-
224
- - **`mode="contain"`** when the whole picture matters (diagrams, maps,
225
- artworks); **`mode="cover"`** for atmosphere (backgrounds, hero slides,
226
- square tiles in a grid).
227
- - **Picture grids**: `BODY.columns(3)` → three `mode="cover"` tiles of
228
- equal size, one caption each.
229
- - **Caption every photo** that is not purely decorative: what it shows and
230
- the source, 12–14 pt, `P.muted`.
231
- - Leave pictures unframed. A `P.surface` background behind a letter-boxed
232
- `contain` picture is the one exception, so the empty band reads as a
233
- frame rather than a gap.
234
-
235
- ## 8. Tables, charts, diagrams
236
-
237
- **Tables** — structure carries the styling:
238
-
239
- ```python
240
- tbl = slide.shapes.add_table(rows, cols, *BODY.inset(bottom=Inches(1.0)), style="clean").table
241
- tbl.format_cells(rows=0, fill=P.ink, color=P.paper, bold=True, size_pt=18)
242
- tbl.format_cells(rows=slice(1, None), size_pt=18, color=P.ink)
243
- tbl.format_cells(rows=slice(1, None, 2), fill=P.surface) # zebra
244
- tbl.format_cells(rows=slice(1, None), cols=0, bold=True) # row labels
245
- ```
246
-
247
- ≤ 6 body rows and ≤ 4 columns on a slide; more belongs in a handout. The
248
- header row is the accent of the slide — use `P.ink` or `P.accent`, not
249
- both.
250
-
251
- **Diagrams** — `pptx2.diagrams` recipes already space nodes and route
252
- arrows; feed them the palette:
253
-
254
- ```python
255
- from pptx2.diagrams import horizontal_pipeline, cycle, hub_and_spoke, comparison_columns
256
-
257
- horizontal_pipeline(slide, BODY.inset(top=Inches(1.4), bottom=Inches(1.4)),
258
- steps=["Vstupy", "Světlo + chlorofyl", "Výstupy"],
259
- accent=P.accent, fill=P.surface, text_color=P.ink,
260
- card_line=None, card_radius=10, size_pt=20)
261
- ```
262
-
263
- A pipeline with 3–5 steps, a cycle with 3–6 nodes, a hub with 3–6 spokes.
264
- Node labels are 2–4 words; explanations go in a list or a card beside the
265
- diagram.
266
-
267
- **Charts** — `chart.recolour([P.accent, P.muted, P.line])`, gridlines in
268
- `P.line`, one series in `P.accent` and the rest in greys when there is a
269
- story to tell. Title the chart with the takeaway ("Kyslík roste s
270
- osvětlením"), not the variable names.
271
-
272
- **Equations** — `slide.shapes.add_equation(bb, latex=..., size_pt=28)` or
273
- `paragraph.add_math(...)`; display equations at 28–36 pt, centred, with
274
- air around them.
275
-
276
- ## 9. Overlaps: deliberate and accidental
277
-
278
- Overlap is a normal design move — text on a card, a caption on a photo, a
279
- translucent band over a hero image, a number disc on a timeline. The
280
- linter's `ShapeCollision` warning asks one question: *did you mean it?*
281
-
282
- - **Meant it** → say so, and the warning disappears:
283
-
284
- ```python
285
- with slide.design_group("hero"): # everything created inside is one cluster
286
- add_picture_fit(slide, "forest.jpg", BBox.from_slide(slide), mode="cover")
287
- band = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, *BBox.from_inches(0, H - 2.2, W, 2.2))
288
- slide.shapes.add_text(BBox.from_inches(0.8, H - 1.9, W - 1.6, 1.6), text="…")
289
-
290
- slide.lint_group_overlaps(card, picture, caption) # shapes already made, widest first
291
- badge.allow_overlap_with(card) # exactly this pair
292
- card.layer, badge.layer_above = "card", "card" # also asserts z-order
293
- ```
294
-
295
- `add_card`, `add_picture_fit`, the components and the diagram recipes
296
- already tag their own shapes, so a card and its text never trigger it.
297
-
298
- - **Did not mean it** → it is a layout bug: two text boxes crossing, a
299
- card poking into its neighbour, a picture over a title. Fix the
300
- geometry (use `columns()` / `split_h()` instead of hand arithmetic) and
301
- re-lint.
302
-
303
- Deliberate stacking looks deliberate when the upper shape is clearly
304
- *inside* the lower one (inset on every side) or clearly *anchored* to one
305
- of its corners/edges. A shape that half-crosses a boundary reads as a
306
- mistake even when it is intentional.
307
-
308
- `slide.tidy()` runs the safe auto-fixes (`OffSlide`, `TextOverflow`,
309
- `OffGridDrift`); collisions, contrast and minimum size stay as warnings
310
- because they need a judgment call. Make it, then move on.
311
-
312
- ## 10. Archetype catalog
313
-
314
- Each archetype below is complete: the code runs given the setup in §2 and
315
- `P`/`D` from §4. Mix 4–5 of them across a deck.
316
-
317
- ```python
318
- def paper(slide, color):
319
- slide.background.fill.solid()
320
- slide.background.fill.fore_color.rgb = color
321
-
322
- def content_slide(title):
323
- """Title band + body region. Every content archetype starts here."""
324
- s = prs.slides.add_slide(BLANK)
325
- paper(s, P.paper)
326
- s.shapes.add_text(TITLE, text=title, size_pt=32, bold=True,
327
- color=P.ink, anchor="middle", margin_pt=0)
328
- return s
329
- ```
330
-
331
- **Title (dark)** — hero title, one-line subtitle, nothing else.
332
-
333
- ```python
334
- s = prs.slides.add_slide(BLANK); paper(s, D.paper)
335
- s.shapes.add_text(BBox.from_inches(0.9, 2.2, W - 1.8, 1.6), text="Fotosyntéza",
336
- size_pt=54, bold=True, color=D.ink, anchor="bottom", margin_pt=0)
337
- s.shapes.add_text(BBox.from_inches(0.9, 3.9, W - 1.8, 0.8),
338
- text="Jak rostliny mění světlo v život · 7. ročník",
339
- size_pt=22, color=D.muted, margin_pt=0)
340
- ```
341
-
342
- **Section divider (dark)** — number + section name, optically centred.
343
-
344
- ```python
345
- s = prs.slides.add_slide(BLANK); paper(s, D.paper)
346
- s.shapes.add_text(BBox.from_inches(0.9, 2.4, W - 1.8, 0.7), text="Část 2",
347
- size_pt=20, color=D.accent, margin_pt=0)
348
- s.shapes.add_text(BBox.from_inches(0.9, 3.0, W - 1.8, 1.4), text="Co se děje v listu",
349
- size_pt=44, bold=True, color=D.ink, margin_pt=0)
350
- ```
351
-
352
- **Statement** — one sentence the audience should remember; optional
353
- follow-up question in the accent.
354
-
355
- ```python
356
- s = prs.slides.add_slide(BLANK); paper(s, P.paper)
357
- s.shapes.add_text(BBox.from_inches(1.2, 1.8, W - 2.4, 3.2),
358
- text="Každý nádech kyslíku kdysi vyrobila rostlina.",
359
- size_pt=40, bold=True, color=P.ink, anchor="middle", align="center")
360
- s.shapes.add_text(BBox.from_inches(1.2, 5.2, W - 2.4, 0.6),
361
- text="Proč je fotosyntéza nejdůležitější reakce na Zemi?",
362
- size_pt=18, color=P.accent, align="center")
363
- ```
364
-
365
- **Bullets + picture** — the workhorse. List left (7/12), picture right
366
- (5/12), caption under the picture.
367
-
368
- ```python
369
- s = content_slide("Co rostlina potřebuje")
370
- left, right = BODY.split_h([7, 5], gap=Inches(0.6))
371
- add_bullets(s, left, items=[
372
- "Světlo — energii dodává Slunce",
373
- "Vodu — kořeny ji přivádějí z půdy",
374
- "Oxid uhličitý — vstupuje průduchy",
375
- "Chlorofyl — zelené barvivo, které světlo zachytí",
376
- ], size_pt=22, color=P.ink)
377
- add_picture_fit(s, "leaf.jpg", right, mode="contain",
378
- caption="List buku · Wikimedia Commons", caption_color=P.muted)
379
- ```
380
-
381
- **Three cards** — parallel concepts, equal weight.
382
-
383
- ```python
384
- s = content_slide("Tři fáze v jednom listu")
385
- for cell, (t, b) in zip(BODY.columns(3, gap=Inches(0.5)), [
386
- ("1 · Zachycení", "Chlorofyl v chloroplastech pohltí světelnou energii."),
387
- ("2 · Rozklad vody", "Energie rozdělí vodu na kyslík a vodík."),
388
- ("3 · Tvorba cukru", "Vodík se spojí s CO₂ a vzniká glukóza."),
389
- ]):
390
- add_card(s, cell, title=t, body=b, fill=P.surface,
391
- title_color=P.ink, body_color=P.ink, title_size_pt=22, body_size_pt=18)
392
- ```
393
-
394
- **Process** — steps with arrows, vertically centred in the body.
395
-
396
- ```python
397
- s = content_slide("Rovnice fotosyntézy jako proces")
398
- horizontal_pipeline(s, BODY.inset(top=Inches(1.4), bottom=Inches(1.4)),
399
- steps=["6 CO₂ + 6 H₂O", "světlo + chlorofyl", "C₆H₁₂O₆ + 6 O₂"],
400
- accent=P.accent, fill=P.surface, text_color=P.ink,
401
- card_line=None, card_radius=10, size_pt=20)
402
- ```
403
-
404
- **Comparison table** — two things side by side, row labels bold.
405
-
406
- ```python
407
- s = content_slide("Fotosyntéza vs. dýchání")
408
- rows = [("", "Fotosyntéza", "Dýchání"), ("Kde", "chloroplasty", "mitochondrie"),
409
- ("Kdy", "jen na světle", "stále"), ("Vstup", "CO₂ + H₂O", "glukóza + O₂"),
410
- ("Výstup", "glukóza + O₂", "CO₂ + H₂O + energie")]
411
- tbl = s.shapes.add_table(len(rows), 3, *BODY.inset(bottom=Inches(1.0)), style="clean").table
412
- for r, row in enumerate(rows):
413
- for c, val in enumerate(row):
414
- tbl.cell(r, c).text = val
415
- tbl.format_cells(rows=0, fill=P.ink, color=P.paper, bold=True, size_pt=18)
416
- tbl.format_cells(rows=slice(1, None), size_pt=18, color=P.ink)
417
- tbl.format_cells(rows=slice(1, None), cols=0, bold=True)
418
- ```
419
-
420
- **Two-column contrast** — "before / after", "myth / fact", "pros / cons".
421
-
422
- ```python
423
- s = content_slide("Mýtus a skutečnost")
424
- a, b = BODY.columns(2, gap=Inches(0.6))
425
- add_card(s, a, title="Mýtus", body="Rostliny dýchají jen v noci.",
426
- fill=P.surface, title_color=P.muted, body_color=P.ink, title_size_pt=20, body_size_pt=20)
427
- add_card(s, b, title="Skutečnost", body="Dýchají nepřetržitě; ve dne fotosyntéza převažuje.",
428
- fill=P.accent_soft, title_color=P.ink, body_color=P.ink, title_size_pt=20, body_size_pt=20)
429
- ```
430
-
431
- **Full-bleed picture with a title band** — see §7 (`mode="cover"` + a
432
- translucent band in `D.paper` + title in `D.ink`).
433
-
434
- **Quote** — large italic statement, attribution in `muted`.
435
-
436
- ```python
437
- s = prs.slides.add_slide(BLANK); paper(s, P.paper)
438
- s.shapes.add_text(BBox.from_inches(1.4, 1.8, W - 2.8, 3.0),
439
- text="„Rostlina je stroj na slunce.“", size_pt=40, italic=True,
440
- color=P.ink, anchor="middle", align="center")
441
- s.shapes.add_text(BBox.from_inches(1.4, 4.9, W - 2.8, 0.6), text="— Jan Evangelista Purkyně",
442
- size_pt=18, color=P.muted, align="center")
443
- ```
444
-
445
- **Question / activity** — one callout card, numbered prompts.
446
-
447
- ```python
448
- s = content_slide("Zamyslete se")
449
- add_card(s, BODY.inset(left=Inches(1.5), right=Inches(1.5), top=Inches(0.6), bottom=Inches(0.6)),
450
- title="Otázka do dvojic", body=[
451
- "Proč jsou listy zelené, když černá pohltí nejvíce světla?",
452
- "Co by se stalo s kyslíkem, kdyby zmizely oceánské řasy?",
453
- ], numbered=True, fill=P.accent_soft, title_color=P.ink, body_color=P.ink,
454
- title_size_pt=24, body_size_pt=20, pad_pt=28)
455
- ```
456
-
457
- **Summary / closing (dark)** — three takeaways at 24 pt, then stop.
458
-
459
- ```python
460
- s = prs.slides.add_slide(BLANK); paper(s, D.paper)
461
- s.shapes.add_text(BBox.from_inches(0.9, 1.4, W - 1.8, 1.0), text="Shrnutí",
462
- size_pt=36, bold=True, color=D.ink, margin_pt=0)
463
- add_bullets(s, BBox.from_inches(0.9, 2.6, W - 1.8, 3.6), items=[
464
- "Rostliny přeměňují světlo, vodu a CO₂ na cukr a kyslík.",
465
- "Děje se to v chloroplastech díky chlorofylu.",
466
- "Bez fotosyntézy by nebyl kyslík ani potrava.",
467
- ], size_pt=24, color=D.ink)
468
- ```
469
-
470
- ## 11. Look at it before you ship it
471
-
472
- Text previews cannot show a clipped title or a lopsided slide. Render the
473
- deck once and look:
474
-
475
- ```python
476
- for s in prs.slides:
477
- s.tidy()
478
- prs.save("output/deck.pptx")
479
- prs.render_contact_sheet("preview.png", cols=4, thumb_width=480) # one PNG, every slide
480
- ```
481
-
482
- What to check on the sheet, in order:
483
-
484
- 1. **Fit** — every title on one line, no text touching a box edge, no
485
- list shrunk to small type.
486
- 2. **Rhythm** — titles at the same height on every content slide; the same
487
- left edge everywhere; dark slides where you intended them.
488
- 3. **Density** — no slide markedly fuller than its neighbours; no half-empty
489
- slide next to a packed one.
490
- 4. **Colour** — one accent per slide; nothing outside the palette except
491
- photos.
492
- 5. **Pictures** — undistorted, cropped where intended, captioned.
493
-
494
- Fix what you see, re-render once, save. Two passes are enough; polishing
495
- beyond that is time better spent on the words.
1
+ # Slide design: making a generated deck look designed
2
+
3
+ `space-aware-authoring.md` makes a deck *physically* correct — nothing
4
+ overflows, nothing falls off the slide. This file is about the other
5
+ half: a deck that looks like a designer laid it out. Everything here is
6
+ built from the public API (`BBox`, `add_text`, `add_card`, `add_bullets`,
7
+ `add_picture_fit`, `PALETTES`, `pptx2.diagrams`, `render_contact_sheet`),
8
+ so the code samples run as written.
9
+
10
+ Read this when the deck will be *seen* — a lesson, a talk, a pitch — and
11
+ not only parsed.
12
+
13
+ ## The one-paragraph brief
14
+
15
+ A slide that looks designed has **one idea**, **one focal point**, **one
16
+ palette**, **one type family**, generous **air** around everything, and
17
+ text that **fits at a readable size**. The viewer knows within a second
18
+ where to look first. Every element on the slide is *content* (a title, a
19
+ claim, a list, a picture, a table, a diagram, an equation) or *structure*
20
+ (a surface, a rule, a caption); there is nothing whose only job is to
21
+ decorate. When the content is strong the slide needs nothing else, and
22
+ when the content is thin, ornament will not save it.
23
+
24
+ ## 1. Start from the content
25
+
26
+ Before drawing anything, write down for each slide:
27
+
28
+ - **Purpose** — one sentence: what the viewer should know or feel after it.
29
+ - **Carrier** — the single element that delivers the purpose: a
30
+ statement, a short list, a picture, a table, a process, an equation.
31
+ - **Support** — at most two more elements that help the carrier (a
32
+ caption, a source, a question, a picture beside a list).
33
+ - **Archetype** — pick one from the catalog in §10. Reusing a handful of
34
+ archetypes is what makes a deck feel consistent.
35
+
36
+ Write the archetype sequence down before you write slide 2 (a comment at
37
+ the top of `main.py` is enough): a deck of 8–12 slides needs at least
38
+ five distinct archetypes, consecutive slides never share one, and no
39
+ archetype appears more than three times. Alternate dense and light
40
+ slides: after a table or a three-card slide, give the audience a
41
+ statement, a big number or a full-bleed picture. A deck whose content
42
+ slides are all "title + bullets" (or all "title + three cards") is not a
43
+ designed deck, however clean each slide is — rebuild it.
44
+
45
+ Plan the pictures at the same time. A hero photo on the title (or on the
46
+ most visual slide) plus a picture on roughly every third content slide
47
+ is the baseline for a classroom deck; fetch them before writing the
48
+ script so the layout is built around them rather than patched later.
49
+
50
+ ### Who is in the room
51
+
52
+ Fix the audience before the first slide: nationality and language
53
+ (Czech, Slovak, Polish, …), school level and age, subject. Everything
54
+ downstream follows from it — the language and its native typography
55
+ (diacritics, „quotation marks“, decimal comma, local dates and
56
+ currency), the examples, names, places, institutions and curriculum
57
+ terms the students recognise (Czech: RVP/ŠVP, Kč, Praha, Karel IV.;
58
+ Slovak: ŠVP, Bratislava, Štúr; Polish: podstawa programowa, zł, Kraków,
59
+ Kopernik), the vocabulary pitched to the age group. Titles are claims
60
+ ("Led je lehčí než voda"), not labels ("Hustota"); every bullet carries a
61
+ concrete fact, number, example or question. A slide that would fit any
62
+ school in any country is not finished.
63
+
64
+ ## 2. Canvas, margins, grid
65
+
66
+ Work in 16:9 at 13.333 × 7.5 in, and set it explicitly:
67
+
68
+ ```python
69
+ from pptx2 import Presentation, BBox
70
+ from pptx2.util import Inches
71
+
72
+ prs = Presentation()
73
+ prs.slide_width, prs.slide_height = Inches(13.333), Inches(7.5)
74
+ W, H = 13.333, 7.5
75
+ BLANK = prs.slide_layouts[6]
76
+ ```
77
+
78
+ Then let two regions govern every content slide:
79
+
80
+ ```python
81
+ TITLE = BBox.from_inches(0.8, 0.55, W - 1.6, 0.9) # title band
82
+ BODY = BBox.from_inches(0.8, 1.7, W - 1.6, H - 2.4) # content area (ends at 6.8")
83
+ ```
84
+
85
+ - **Margins**: 0.8 in left/right, 0.55 in top, 0.7 in bottom. Nothing but
86
+ a full-bleed picture or a coloured background touches the slide edge.
87
+ - **Gutters**: 0.4–0.6 in between columns and cards. Use one value per deck.
88
+ - **Columns**: `BODY.columns(3, gap=Inches(0.5))`,
89
+ `BODY.split_h([7, 5], gap=Inches(0.6))`, `BODY.rows(2, gap=...)`. Every
90
+ card, column and picture on a slide starts at the same left edge as the
91
+ title, or at a grid line derived from it.
92
+ - **Optical center**: single statements and hero titles sit slightly
93
+ above the geometric middle (`anchor="middle"` in a box whose top is
94
+ ~1.6 in and bottom ~5.4 in).
95
+
96
+ ## 3. Typography
97
+
98
+ One family for the whole deck. Noto Sans (or whatever the environment's
99
+ default sans is) is a fine choice; the design comes from *scale* and
100
+ *spacing*, not from the font.
101
+
102
+ | Role | Size | Weight | Notes |
103
+ |---|---|---|---|
104
+ | Hero title (title / section slide) | 48–60 pt | bold | 1–2 lines |
105
+ | Statement (the one-sentence slide) | 36–44 pt | bold | ≤ 20 words, centred or left |
106
+ | Big number | 96–120 pt | bold, `accent` | one figure + a 24 pt explanation |
107
+ | Slide title | 32–36 pt (never below 28) | bold | one line; sentence case |
108
+ | Body / bullets | 22–26 pt (never below 20) | regular | ≤ 6 items, ≤ 12 words each |
109
+ | Card title | 22–24 pt | bold | |
110
+ | Card body | 18–20 pt (never below 18) | regular | 2–4 lines |
111
+ | Table cells | 18–20 pt | header bold | |
112
+ | Caption / source / footer | 14 pt | regular or italic, `muted` | |
113
+
114
+ Nothing on a slide is under 14 pt: a projected slide is read from the
115
+ back of a room, and a font that only fits by going smaller is a sign of
116
+ too much text, not of a small box. Cut words or split the slide; never
117
+ solve density with a smaller size.
118
+
119
+ Rules that make type look intentional:
120
+
121
+ - **Sentence case** for titles ("Co rostlina potřebuje", not "CO ROSTLINA
122
+ POTŘEBUJE"). Uppercase belongs to acronyms.
123
+ - **Left-align running text.** Centre only short statements, titles on
124
+ hero slides, and labels inside diagram nodes.
125
+ - **Breathing room**: `line_spacing = 1.1–1.2` for body, `space_after =
126
+ 8–10 pt` between bullets (`add_bullets(gap_pt=8)` does this).
127
+ - **Zero text-box margins** when the box is already placed by the grid
128
+ (`margin_pt=0`) so the text edge *is* the grid line.
129
+ - **Hierarchy by size and weight, not by colour.** Reserve colour for the
130
+ one thing on the slide that should pop (§4).
131
+ - **Fit, then check**: `add_bullets` and `add_card` shrink text to the box
132
+ and never below 18 pt (bullets) / 16 pt (card body). If they had to
133
+ shrink, there is too much text — split the slide. `audit()` reports a
134
+ `MinFontSize` warning for any run under 12 pt.
135
+
136
+ ```python
137
+ slide.shapes.add_text(TITLE, text="Co rostlina potřebuje",
138
+ size_pt=32, bold=True, color=P.ink,
139
+ anchor="middle", margin_pt=0)
140
+ ```
141
+
142
+ ## 4. Colour
143
+
144
+ Pick **one** palette and use only its roles:
145
+
146
+ ```python
147
+ from pptx2 import PALETTES
148
+
149
+ P = PALETTES["slate"] # slate · linen · forest · plum · ember · navy · graphite
150
+ D = P.dark() # same hues on a dark paper — title, section, closing slides
151
+ ```
152
+
153
+ | Role | Use |
154
+ |---|---|
155
+ | `P.paper` | slide background (`slide.background.fill.solid(); ...fore_color.rgb = P.paper`) |
156
+ | `P.ink` | all headings and body text |
157
+ | `P.muted` | captions, sources, secondary labels, table grid text |
158
+ | `P.surface` | card fills, table zebra rows, picture frames |
159
+ | `P.line` | hairline rules and table borders |
160
+ | `P.accent` | **one** emphasis per slide: the key number, the active step, a highlighted word, the header row |
161
+ | `P.accent_soft` | fill of a callout card / highlighted row; text on it stays `P.ink` |
162
+ | `P.accent_ink` | text placed *on* `P.accent` (white on a dark accent) |
163
+
164
+ - **One accent per slide.** Two accents split attention; three look like a
165
+ toy. Cards in a row all share `P.surface`; the one to emphasise (if any)
166
+ gets `P.accent_soft`.
167
+ - **Dark slides** for the title, section dividers and the closing slide
168
+ give the deck rhythm. Use `D = P.dark()` so the hues stay in the family.
169
+ - **Contrast**: every text/background pair ≥ 4.5:1 (the linter reports
170
+ `LowContrast`); the shipped palettes satisfy this for `ink` on `paper`,
171
+ `surface` and `accent_soft`, for `muted` on `paper`, and for `accent_ink`
172
+ on `accent`.
173
+ - **Photos bring their own colour.** Beside a photo, keep the rest of the
174
+ slide to `paper` / `ink` / `muted`.
175
+
176
+ ## 5. Surfaces: cards, callouts, panels
177
+
178
+ A card is one tinted rounded rectangle with padded text inside it, and
179
+ that is the whole card. Its silhouette stays a clean rectangle from every
180
+ angle; emphasis comes from the tint (or, when the tint is unwanted, a
181
+ hairline outline — one or the other, never both). Padding of 18–28 pt on
182
+ every side gives the text room to sit rather than press against the edge.
183
+
184
+ ```python
185
+ from pptx2 import add_card
186
+
187
+ for cell, (title, body) in zip(BODY.columns(3, gap=Inches(0.5)), items):
188
+ add_card(slide, cell, title=title, body=body,
189
+ fill=P.surface, title_color=P.ink, body_color=P.ink,
190
+ title_size_pt=22, body_size_pt=18, pad_pt=22)
191
+ ```
192
+
193
+ Composition rules for groups of cards:
194
+
195
+ - **2–4 cards per row**, equal size, equal gap, same fill, same radius.
196
+ Five or more become a table.
197
+ - **Same content shape in every card**: if one has a title and two lines,
198
+ all have a title and two lines. Trim copy to match.
199
+ - **A callout** (question, key definition, "remember this") is one wide
200
+ card in `P.accent_soft`, inset from the body region so it reads as a
201
+ deliberate object, not as a background.
202
+ - **Cards hold content, backgrounds hold cards.** A card sits directly on
203
+ the paper; it does not sit on another panel.
204
+ - **Numbering** belongs *in* the title text ("1 · Zachycení"), where it
205
+ aligns with everything else, rather than in a separate badge shape.
206
+
207
+ `add_card` returns `card.inner` (the padded box) so a picture, equation
208
+ or diagram can be placed inside the same surface:
209
+
210
+ ```python
211
+ c = add_card(slide, right_col, fill=P.surface)
212
+ slide.shapes.add_equation(c.inner, latex=r"6\,CO_2 + 6\,H_2O \rightarrow C_6H_{12}O_6 + 6\,O_2", size_pt=28)
213
+ ```
214
+
215
+ ### Code listings
216
+
217
+ Source code is `add_code`, never a text box with a monospace font: it
218
+ writes one paragraph per line, verbatim, unwrapped, in a monospace face
219
+ on a rounded editor-coloured surface, and *names the shape*
220
+ `fika:code:<language>:<theme>:<0|1>`. An editor with a native code block
221
+ (Fika) turns that shape into a syntax-highlighted, editable code element
222
+ on import; PowerPoint shows a clean monospace box. A listing is a visual
223
+ — it takes a column or the body, 18–20 pt, at most ~12 lines; longer
224
+ code is split across slides, explained beside it, not shrunk.
225
+
226
+ ```python
227
+ left, right = BODY.split_h([5, 7], gap=Inches(0.6))
228
+ add_bullets(s, left, items=["if – když platí podmínka", "else – jinak"], size_pt=22, color=P.ink)
229
+ add_code(s, right, code='if (odpoved == "ano")\n{\n Console.WriteLine("Skvělé!");\n}',
230
+ language="csharp", theme="github-dark", size_pt=18)
231
+ ```
232
+
233
+ ## 6. Whitespace and alignment
234
+
235
+ - Air lives in the margins and gutters — not in a half-empty body. Content
236
+ fills `BODY`: a row of cards spans its full width *and* height
237
+ (`BODY.columns(n)`), two columns split it evenly, a picture takes its
238
+ whole column. Never hand-size a few 3 in boxes and float them in an
239
+ 11.7 in area. When the content is short, use fewer, larger elements
240
+ (two big cards, one big number, a statement) rather than small ones
241
+ with dead space around them.
242
+ - **One left edge.** Title, bullets, cards and pictures share the `0.8 in`
243
+ margin (or a grid line derived from it). Right edges align to the
244
+ mirror margin.
245
+ - **Consistent vertical rhythm.** Title band → 0.25 in gap → body. Every
246
+ content slide places its body at the same `BODY.top`, so flipping
247
+ through the deck feels steady.
248
+ - **Balance, not symmetry.** A picture on the right with a list on the
249
+ left is balanced when the list's text block is roughly as tall as the
250
+ picture. Use `split_h([7, 5])` for text-heavy pairs, `[1, 1]` for a
251
+ true comparison.
252
+ - **Empty is fine, unbalanced is not.** A slide whose right half is blank
253
+ because the list is short: make the list larger (24 pt), add a picture,
254
+ or narrow the text column and centre the pair.
255
+
256
+ ## 7. Pictures
257
+
258
+ Photos carry emotion and specificity — one good photo does more than three
259
+ small ones.
260
+
261
+ ```python
262
+ from pptx2 import add_picture_fit
263
+
264
+ # Beside text: fit inside the column, never distort, caption in muted.
265
+ add_picture_fit(slide, "leaf.jpg", right_col, mode="contain",
266
+ caption="Buk lesní · Wikimedia Commons", caption_color=P.muted)
267
+
268
+ # Full-bleed hero: crop to fill the whole slide, then a dark band for text.
269
+ with slide.design_group("hero"): # photo + band + title are one deliberate stack
270
+ add_picture_fit(slide, "forest.jpg", BBox.from_inches(0, 0, W, H), mode="cover")
271
+ band = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, *BBox.from_inches(0, H - 2.2, W, 2.2))
272
+ band.fill_hex(D.paper); band.line.fill.background(); band.shadow.clear()
273
+ band.fill.fore_color.alpha = 0.8 # translucent, photo shows through
274
+ slide.shapes.add_text(BBox.from_inches(0.8, H - 1.9, W - 1.6, 1.6),
275
+ text="Les jako plíce planety", size_pt=44, bold=True,
276
+ color=D.ink, anchor="middle", margin_pt=0)
277
+ ```
278
+
279
+ - **`mode="contain"`** when the whole picture matters (diagrams, maps,
280
+ artworks); **`mode="cover"`** for atmosphere (backgrounds, hero slides,
281
+ square tiles in a grid).
282
+ - **Picture grids**: `BODY.columns(3)` → three `mode="cover"` tiles of
283
+ equal size, one caption each.
284
+ - **Caption every photo** that is not purely decorative: what it shows and
285
+ the source, 12–14 pt, `P.muted`.
286
+ - Leave pictures unframed. A `P.surface` background behind a letter-boxed
287
+ `contain` picture is the one exception, so the empty band reads as a
288
+ frame rather than a gap.
289
+
290
+ ## 8. Tables, charts, diagrams
291
+
292
+ **Tables** — structure carries the styling:
293
+
294
+ ```python
295
+ tbl = slide.shapes.add_table(rows, cols, *BODY.inset(bottom=Inches(1.0)), style="clean").table
296
+ tbl.format_cells(rows=0, fill=P.ink, color=P.paper, bold=True, size_pt=18)
297
+ tbl.format_cells(rows=slice(1, None), size_pt=18, color=P.ink)
298
+ tbl.format_cells(rows=slice(1, None, 2), fill=P.surface) # zebra
299
+ tbl.format_cells(rows=slice(1, None), cols=0, bold=True) # row labels
300
+ ```
301
+
302
+ ≤ 6 body rows and ≤ 4 columns on a slide; more belongs in a handout. The
303
+ header row is the accent of the slide — use `P.ink` or `P.accent`, not
304
+ both.
305
+
306
+ **Diagrams** — `pptx2.diagrams` recipes already space nodes and route
307
+ arrows; feed them the palette:
308
+
309
+ ```python
310
+ from pptx2.diagrams import horizontal_pipeline, cycle, hub_and_spoke, comparison_columns
311
+
312
+ horizontal_pipeline(slide, BODY.inset(top=Inches(1.4), bottom=Inches(1.4)),
313
+ steps=["Vstupy", "Světlo + chlorofyl", "Výstupy"],
314
+ accent=P.accent, fill=P.surface, text_color=P.ink,
315
+ card_line=None, card_radius=10, size_pt=20)
316
+ ```
317
+
318
+ A pipeline with 3–5 steps, a cycle with 3–6 nodes, a hub with 3–6 spokes.
319
+ Node labels are 2–4 words; explanations go in a list or a card beside the
320
+ diagram.
321
+
322
+ **Charts** — `chart.recolour([P.accent, P.muted, P.line])`, gridlines in
323
+ `P.line`, one series in `P.accent` and the rest in greys when there is a
324
+ story to tell. Title the chart with the takeaway ("Kyslík roste s
325
+ osvětlením"), not the variable names.
326
+
327
+ **Equations** — `slide.shapes.add_equation(bb, latex=..., size_pt=28)` or
328
+ `paragraph.add_math(...)`; display equations at 28–36 pt, centred, with
329
+ air around them.
330
+
331
+ ## 9. Overlaps: deliberate and accidental
332
+
333
+ Overlap is a normal design move — text on a card, a caption on a photo, a
334
+ translucent band over a hero image, a number disc on a timeline. The
335
+ linter's `ShapeCollision` warning asks one question: *did you mean it?*
336
+
337
+ - **Meant it** → say so, and the warning disappears:
338
+
339
+ ```python
340
+ with slide.design_group("hero"): # everything created inside is one cluster
341
+ add_picture_fit(slide, "forest.jpg", BBox.from_slide(slide), mode="cover")
342
+ band = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, *BBox.from_inches(0, H - 2.2, W, 2.2))
343
+ slide.shapes.add_text(BBox.from_inches(0.8, H - 1.9, W - 1.6, 1.6), text="…")
344
+
345
+ slide.lint_group_overlaps(card, picture, caption) # shapes already made, widest first
346
+ badge.allow_overlap_with(card) # exactly this pair
347
+ card.layer, badge.layer_above = "card", "card" # also asserts z-order
348
+ ```
349
+
350
+ `add_card`, `add_picture_fit`, the components and the diagram recipes
351
+ already tag their own shapes, so a card and its text never trigger it.
352
+
353
+ - **Did not mean it** → it is a layout bug: two text boxes crossing, a
354
+ card poking into its neighbour, a picture over a title. Fix the
355
+ geometry (use `columns()` / `split_h()` instead of hand arithmetic) and
356
+ re-lint.
357
+
358
+ Deliberate stacking looks deliberate when the upper shape is clearly
359
+ *inside* the lower one (inset on every side) or clearly *anchored* to one
360
+ of its corners/edges. A shape that half-crosses a boundary reads as a
361
+ mistake even when it is intentional.
362
+
363
+ `slide.tidy()` runs the safe auto-fixes (`OffSlide`, `TextOverflow`,
364
+ `OffGridDrift`); collisions, contrast and minimum size stay as warnings
365
+ because they need a judgment call. Make it, then move on.
366
+
367
+ ## 10. Archetype catalog
368
+
369
+ Each archetype below is complete: the code runs given the setup in §2 and
370
+ `P`/`D` from §4. Mix 4–5 of them across a deck.
371
+
372
+ ```python
373
+ def paper(slide, color):
374
+ slide.background.fill.solid()
375
+ slide.background.fill.fore_color.rgb = color
376
+
377
+ def content_slide(title):
378
+ """Title band + body region. Every content archetype starts here."""
379
+ s = prs.slides.add_slide(BLANK)
380
+ paper(s, P.paper)
381
+ s.shapes.add_text(TITLE, text=title, size_pt=32, bold=True,
382
+ color=P.ink, anchor="middle", margin_pt=0)
383
+ return s
384
+ ```
385
+
386
+ **Title (dark)** — hero title, one-line subtitle, nothing else.
387
+
388
+ ```python
389
+ s = prs.slides.add_slide(BLANK); paper(s, D.paper)
390
+ s.shapes.add_text(BBox.from_inches(0.9, 2.2, W - 1.8, 1.6), text="Fotosyntéza",
391
+ size_pt=54, bold=True, color=D.ink, anchor="bottom", margin_pt=0)
392
+ s.shapes.add_text(BBox.from_inches(0.9, 3.9, W - 1.8, 0.8),
393
+ text="Jak rostliny mění světlo v život · 7. ročník",
394
+ size_pt=22, color=D.muted, margin_pt=0)
395
+ ```
396
+
397
+ **Section divider (dark)** — number + section name, optically centred.
398
+
399
+ ```python
400
+ s = prs.slides.add_slide(BLANK); paper(s, D.paper)
401
+ s.shapes.add_text(BBox.from_inches(0.9, 2.4, W - 1.8, 0.7), text="Část 2",
402
+ size_pt=20, color=D.accent, margin_pt=0)
403
+ s.shapes.add_text(BBox.from_inches(0.9, 3.0, W - 1.8, 1.4), text="Co se děje v listu",
404
+ size_pt=44, bold=True, color=D.ink, margin_pt=0)
405
+ ```
406
+
407
+ **Statement** — one sentence the audience should remember; optional
408
+ follow-up question in the accent.
409
+
410
+ ```python
411
+ s = prs.slides.add_slide(BLANK); paper(s, P.paper)
412
+ s.shapes.add_text(BBox.from_inches(1.2, 1.8, W - 2.4, 3.2),
413
+ text="Každý nádech kyslíku kdysi vyrobila rostlina.",
414
+ size_pt=40, bold=True, color=P.ink, anchor="middle", align="center")
415
+ s.shapes.add_text(BBox.from_inches(1.2, 5.2, W - 2.4, 0.6),
416
+ text="Proč je fotosyntéza nejdůležitější reakce na Zemi?",
417
+ size_pt=18, color=P.accent, align="center")
418
+ ```
419
+
420
+ **Bullets + picture** — the workhorse. List left (7/12), picture right
421
+ (5/12), caption under the picture.
422
+
423
+ ```python
424
+ s = content_slide("Co rostlina potřebuje")
425
+ left, right = BODY.split_h([7, 5], gap=Inches(0.6))
426
+ add_bullets(s, left, items=[
427
+ "Světlo — energii dodává Slunce",
428
+ "Vodu — kořeny ji přivádějí z půdy",
429
+ "Oxid uhličitý — vstupuje průduchy",
430
+ "Chlorofyl — zelené barvivo, které světlo zachytí",
431
+ ], size_pt=22, color=P.ink)
432
+ add_picture_fit(s, "leaf.jpg", right, mode="contain",
433
+ caption="List buku · Wikimedia Commons", caption_color=P.muted)
434
+ ```
435
+
436
+ **Three cards** — parallel concepts, equal weight.
437
+
438
+ ```python
439
+ s = content_slide("Tři fáze v jednom listu")
440
+ for cell, (t, b) in zip(BODY.columns(3, gap=Inches(0.5)), [
441
+ ("1 · Zachycení", "Chlorofyl v chloroplastech pohltí světelnou energii."),
442
+ ("2 · Rozklad vody", "Energie rozdělí vodu na kyslík a vodík."),
443
+ ("3 · Tvorba cukru", "Vodík se spojí s CO₂ a vzniká glukóza."),
444
+ ]):
445
+ add_card(s, cell, title=t, body=b, fill=P.surface,
446
+ title_color=P.ink, body_color=P.ink, title_size_pt=22, body_size_pt=18)
447
+ ```
448
+
449
+ **Process** — steps with arrows, vertically centred in the body.
450
+
451
+ ```python
452
+ s = content_slide("Rovnice fotosyntézy jako proces")
453
+ horizontal_pipeline(s, BODY.inset(top=Inches(1.4), bottom=Inches(1.4)),
454
+ steps=["6 CO₂ + 6 H₂O", "světlo + chlorofyl", "C₆H₁₂O₆ + 6 O₂"],
455
+ accent=P.accent, fill=P.surface, text_color=P.ink,
456
+ card_line=None, card_radius=10, size_pt=20)
457
+ ```
458
+
459
+ **Comparison table** — two things side by side, row labels bold.
460
+
461
+ ```python
462
+ s = content_slide("Fotosyntéza vs. dýchání")
463
+ rows = [("", "Fotosyntéza", "Dýchání"), ("Kde", "chloroplasty", "mitochondrie"),
464
+ ("Kdy", "jen na světle", "stále"), ("Vstup", "CO₂ + H₂O", "glukóza + O₂"),
465
+ ("Výstup", "glukóza + O₂", "CO₂ + H₂O + energie")]
466
+ tbl = s.shapes.add_table(len(rows), 3, *BODY.inset(bottom=Inches(1.0)), style="clean").table
467
+ for r, row in enumerate(rows):
468
+ for c, val in enumerate(row):
469
+ tbl.cell(r, c).text = val
470
+ tbl.format_cells(rows=0, fill=P.ink, color=P.paper, bold=True, size_pt=18)
471
+ tbl.format_cells(rows=slice(1, None), size_pt=18, color=P.ink)
472
+ tbl.format_cells(rows=slice(1, None), cols=0, bold=True)
473
+ ```
474
+
475
+ **Two-column contrast** — "before / after", "myth / fact", "pros / cons".
476
+
477
+ ```python
478
+ s = content_slide("Mýtus a skutečnost")
479
+ a, b = BODY.columns(2, gap=Inches(0.6))
480
+ add_card(s, a, title="Mýtus", body="Rostliny dýchají jen v noci.",
481
+ fill=P.surface, title_color=P.muted, body_color=P.ink, title_size_pt=20, body_size_pt=20)
482
+ add_card(s, b, title="Skutečnost", body="Dýchají nepřetržitě; ve dne fotosyntéza převažuje.",
483
+ fill=P.accent_soft, title_color=P.ink, body_color=P.ink, title_size_pt=20, body_size_pt=20)
484
+ ```
485
+
486
+ **Full-bleed picture with a title band** — see §7 (`mode="cover"` + a
487
+ translucent band in `D.paper` + title in `D.ink`).
488
+
489
+ **Quote** — large italic statement, attribution in `muted`.
490
+
491
+ ```python
492
+ s = prs.slides.add_slide(BLANK); paper(s, P.paper)
493
+ s.shapes.add_text(BBox.from_inches(1.4, 1.8, W - 2.8, 3.0),
494
+ text="„Rostlina je stroj na slunce.“", size_pt=40, italic=True,
495
+ color=P.ink, anchor="middle", align="center")
496
+ s.shapes.add_text(BBox.from_inches(1.4, 4.9, W - 2.8, 0.6), text="— Jan Evangelista Purkyně",
497
+ size_pt=18, color=P.muted, align="center")
498
+ ```
499
+
500
+ **Question / activity** — one callout card, numbered prompts.
501
+
502
+ ```python
503
+ s = content_slide("Zamyslete se")
504
+ add_card(s, BODY.inset(left=Inches(1.5), right=Inches(1.5), top=Inches(0.6), bottom=Inches(0.6)),
505
+ title="Otázka do dvojic", body=[
506
+ "Proč jsou listy zelené, když černá pohltí nejvíce světla?",
507
+ "Co by se stalo s kyslíkem, kdyby zmizely oceánské řasy?",
508
+ ], numbered=True, fill=P.accent_soft, title_color=P.ink, body_color=P.ink,
509
+ title_size_pt=24, body_size_pt=20, pad_pt=28)
510
+ ```
511
+
512
+ **Summary / closing (dark)** — three takeaways at 24 pt, then stop.
513
+
514
+ ```python
515
+ s = prs.slides.add_slide(BLANK); paper(s, D.paper)
516
+ s.shapes.add_text(BBox.from_inches(0.9, 1.4, W - 1.8, 1.0), text="Shrnutí",
517
+ size_pt=36, bold=True, color=D.ink, margin_pt=0)
518
+ add_bullets(s, BBox.from_inches(0.9, 2.6, W - 1.8, 3.6), items=[
519
+ "Rostliny přeměňují světlo, vodu a CO₂ na cukr a kyslík.",
520
+ "Děje se to v chloroplastech díky chlorofylu.",
521
+ "Bez fotosyntézy by nebyl kyslík ani potrava.",
522
+ ], size_pt=24, color=D.ink)
523
+ ```
524
+
525
+ ## 11. Look at it before you ship it
526
+
527
+ Text previews cannot show a clipped title or a lopsided slide. Render the
528
+ deck once and look:
529
+
530
+ ```python
531
+ for s in prs.slides:
532
+ s.tidy()
533
+ prs.save("output/deck.pptx")
534
+ prs.render_contact_sheet("preview.png", cols=4, thumb_width=480) # one PNG, every slide
535
+ ```
536
+
537
+ What to check on the sheet, in order:
538
+
539
+ 1. **Fit** — every title on one line, no text touching a box edge, no
540
+ list shrunk to small type.
541
+ 2. **Rhythm** — titles at the same height on every content slide; the same
542
+ left edge everywhere; dark slides where you intended them.
543
+ 3. **Density** — no slide markedly fuller than its neighbours; no half-empty
544
+ slide next to a packed one.
545
+ 4. **Colour** — one accent per slide; nothing outside the palette except
546
+ photos.
547
+ 5. **Pictures** — undistorted, cropped where intended, captioned.
548
+
549
+ Fix what you see, re-render once, save. Two passes are enough; polishing
550
+ beyond that is time better spent on the words.