Design and build a deck as a python-pptx program: your own layout system, the source's own figures, charts you draw, and a template's design when the user gave one.
Installs into .claude/skills of the current project.
Are you the author of Ppt Script Authoring?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/evermind-ai-ppt-script-authoring)
---
name: ppt-script-authoring
description: "Design and build a deck as a python-pptx program: your own layout system, the source's own figures, charts you draw, and a template's design when the user gave one."
metadata: {"raven":{"emoji":"🎞️","always":true}}
---
# Deck authoring
You design the deck and you build it, by writing a python-pptx program that
`ppt_build` runs. Every page is yours: the grid, the type scale, the palette, the page
furniture, where a figure sits and how large. What refuses a deck and what only reports
is in §12.
## 1. Define the communication job
Infer the audience, the deck's job, the outcome you want, the central takeaway and
the evidence that supports it. Build a cumulative argument rather than an inventory
of topics: each content page performs one narrative job and makes one primary claim.
Write audience-facing titles that state the point. Open with the context, question
or stakes that make the deck worth sitting through. Close by resolving that opening
— a decision, an implication, a next question — on the last content page. The
template's closing page ends the deck after it, as its cover opens it: both are the
house frame and carry no argument of their own.
Pack the evidence before allocating pages. A page carries one claim and the developed
support that claim takes, and what makes the support developed is that it is more than
one kind of thing: evidence from the source, a mechanism or a comparison, an
implication or a constraint. Nothing counts the kinds, and how much copy a page ends up
with is measured rather than allocated (§2). If the evidence cannot support a complete
argument, merge the page rather than enlarging fragments.
The language, the audience and the length are the user's, and they are asked for
rather than inferred. `ppt_prepare` reads what the request already states and hands
back the rest as questions under `ask_user`. **Put them to the user with the `ask_user`
tool, all of them in one call, each with the option you would recommend marked.** The
answer comes back through the host: in a direct chat the user answers; in a delegated
run the host answers what the delegating conversation already settled and puts the rest
to the user. An answer that is empty, or that says the question was not sent, means
nobody is attached to this run: then take the option you marked recommended -- the
request's own scope, the materials' language, the length its kind of deck usually has --
and record it with `ppt_brief` as the assumption it is, so the deck says what it assumed.
Ask once: a question nobody answered is not answered by asking it again, and a run that
asks a question a turn spends a turn on each.
The length and the language are then measured against the
finished file and both refuse the deck (§12), so record what the user said rather than
what you inferred. The audience is not measured; it is the room you judge the deck for
when you look at the renders (§10).
The fourth thing to record is the one that gets dropped: whatever the user ruled out. `ppt_brief(forbidden=["no icons", "never name a competitor"])` is
quoted back to you in so many words on **every** build, which is what keeps a
prohibition agreed at turn three alive at turn forty; the same thing filed under
`notes` is kept with the brief and never restated. Only what the user actually ruled
out belongs there — a preference you inferred, recorded as a rule, is a constraint
nobody agreed to.
## 2. Write the outline before you write the program
**Go looking for the deck's pictures first, and look widely.** Not for a page but for
the pool the outline gets to choose from. `ppt_image_search` takes every query at once
-- `queries: ["...", "..."]`, up to twelve, searched together and grouped by query in the
reply -- so the whole pool is one call, not one call per picture. It returns each
candidate with its pixel size and the page it came from. Search the things the material
*names* as well as the things it links — a paper, a benchmark, a product, a release —
and the ordinary furniture too, the logo and the product shot, which every deck wants and
no material bothers to link. Bring in more than one page will use: `ppt_fetch` each, and
`ppt_figure_inspect` them, because a candidate you have not looked at is a candidate you
cannot choose.
`ppt_outline` records it, and the build refuses until it exists. This is the stage that
decides how much is on a page: without one, what a page says gets decided while its
geometry is being typed, and the page comes out thin.
Per page: the **claim** as a statement (which is also its title), what **carries**
it, the **figures** it places by id, and what it **says** in the deck's language, as
it will read on the page.
The outline does not decide a page's *shape*. It used to record a table's columns and
cells for any page whose `carries` mentioned a comparison, and a brief that asks for a
「对照表」 is ordinary — so a deck came back with five tables whose every cell was a
phrase, planned before anyone had seen the content laid out. What a page carries is
settled where it is drawn, with
[deck/build/references/tables.md](deck/build/references/tables.md) open. **Figures a
reader compares down a column are a table** — a benchmark, a price list, a
year-by-year rate — and that page should draw one. Words in every cell are cards.
`says` is where density is decided, and it is the field that gets under-written. A page
carries 200 to 467 characters of copy and the **median is 250** — a
five-row comparison, two takeaways and a caveat is that page, and it is a good page.
Plan for the median rather than the top of the range: the top of it is where the type
reaches its floor and `overset_copy` starts reporting. Points that are each half a line
add up to a page that is a title and some labels, and the build cannot rescue it:
geometry can arrange copy, never supply it. So write each point as the sentence a reader
gets, evidence included, and if a page has only one short line in it either the evidence
for it belongs there too or the page belongs merged into its neighbour.
Checking the plan here rather than after the build is the difference between a cheap
edit and an expensive one: what refuses an outline, and what it only reports, is in §12.
And `needs` is where gathering belongs — this is the first moment anything knows
what each page will show. Go through the outline page by page and name the picture
each one wants, then find them all in one `ppt_image_search` (`queries: [...]`, one per
page that wants a picture) and bring in what you pick with one `ppt_fetch`. A fetch joins the deck's own source set and is read on arrival, so there
is no separate ingest to remember; `ppt_ingest` is for bringing in another directory
of the user's, or re-reading after you have edited a source by hand.
A deck whose only imagery is what the materials happened to contain is a deck of
panels: the products it names have logos, the organisations have marks, the standards
have badges, and the architectures have diagrams their own documentation draws. Search
for those before deciding a page is text.
**What a page is missing is searched for, not filled in.** The two paths above gather
pictures; the same move answers a claim the sources leave without its evidence.
`web_search` for it, `web_fetch` the page that carries it, and keep where it came from
beside what you took. Nothing on a page is invented: not a number, not a source, not a
person, not a place. A gap you went looking for and did not find is worth keeping -- say
in `needs` what is missing and who has to supply it -- and a gap nobody looked for is a
page handing its own work back to the reader.
## 3. One visual identity, defined once
**What a finished page owes is written down, and it is what the review at the end judges
by: [deck/build/references/design-requirements.md](deck/build/references/design-requirements.md).
Open it now, before the first page.** Seven short sections -- room, edges, marks, figures,
tables, type, the claim -- each decidable from the render alone. Two of them are here as well:
- **A blank region has two kinds and they have different answers.** A shape bigger than
what it holds is fixed by measuring (`card_size`, `text_size`, `picture_size`,
`table_size`) and taking only that much. A *page* bigger than what it holds is not
fixed by shrinking anything -- taking a cover off room leaves the room -- and wants
either more that the page needs said, or this page and its neighbour becoming one page.
The test is one question: would making the shape smaller fill the page?
- **A table spans the box you hand it**, in the proportions its columns measure, and
spreads its rows into it down to five rows or so. So the box is the whole answer: give
a page-wide table `frame.body` and a two-column one a `split_left` column, never the
body with `weights` guessed to make up the difference.
Take the palette and font from `ppt_theme`. It is written into the build directory on
every build, so the import is plain — that directory is already on `sys.path`:
```python
from ppt_theme import THEMES, rgb
theme_id = next(iter(THEMES)) # one entry, and it is the template's
T = THEMES[theme_id]
BG, SURFACE = rgb(T["background"]), rgb(T["surface"])
INK, MUTED, ACCENT = rgb(T["foreground"]), rgb(T["muted"]), rgb(T["accent"])
SOFT, GRID, ACCENT_INK = rgb(T["accent_soft"]), rgb(T["grid"]), rgb(T["accent_ink"])
SERIES = [rgb(c) for c in T["chart_series"]] # six, in order
FONT = T["font_family"]
```
**Do not write a module into that directory whose name shadows a standard one.** A
`copy.py` beside your script breaks python-pptx's own import, and the traceback will
not mention your file.
**There is no palette to pick, and one to state.** A deck is always built inside a
template: `ppt_prepare` binds the one the user gave, and binds a bundled default when
the user gave none. The build directory then holds one theme, named after that file —
so `THEMES` has exactly one entry and its face is the template's own. Take that entry
by iteration; a theme id typed into `THEMES[...]` is a `KeyError`. Where the renders
show a ground or an accent the file does not declare, state what you see once with
`ppt_template(palette=...)` before the build, and every page and every derived tint
follow from it; a `T["background"] = ...` in the program changes one run's pages and
records nothing.
**Only three roles are read off the file** -- the ground, the ink and its first accent.
`surface`, `accent_soft`, `accent_ink`, `grid` and `muted` are mixed from those three.
What a file declares is not what its pages paint -- all ten bundled templates declare
`#F0F0F0` as their second light colour and none of them paints it -- so say what the
renders show:
`ppt_template(project=..., palette={"accent": "#155FFD", "surface": "#DDE8FF"})` holds
for the deck's whole life and reaches every page through `ppt_theme`.
**A partial palette is the ordinary one.** What you leave out is re-derived from what
you named: `palette={"accent": "#7A1FA2"}` in an amber template brings `surface`,
`accent_soft` and `accent_ink` back as tints of that purple. Reach for it for a deck
with a brand of its own, an accent the template does not carry, or a series the content
decides -- `chart_series` is the one role a stated accent does *not* move, so a
comparison drawn in your own two colours states them. A later call merges over the
earlier one, so state it before the first page.
The theme part is what the build checks, and you pass it by opening `PPT_TEMPLATE` at
all (§12). Nothing checks what colour you filled a shape with.
**The role names are open; a role's meaning is not.** These eight are the ones the
derivation knows how to finish:
| Role | What it is for |
| --- | --- |
| `background` | the ground a page is painted on |
| `surface`, `accent_soft` | planes content sits on |
| `foreground` | body type |
| `muted` | secondary type and source lines |
| `accent` | the focal point of a page |
| `accent_ink` | type in the accent's colour (below) |
| `grid` | hairlines |
You may add your own beside them: a deck comparing two things wants two colours for the
pair, and `palette={"ours": "#0B3D91", "theirs": "#F2E8D5"}` puts both in every page's
theme, where `card(..., tint="ours")` reaches them by name. What may not move is the
meaning — if the accent marks "our result" on one page it cannot mark "prior work" on
the next, and a pair named on page 3 is the same pair on page 11.
**A helper that holds the theme takes a colour by name.** `tint=` and
`colour=`/`mark_colour=` -- the signatures in §4 say which helper takes which -- each
take a role name, a key you added to the palette or to your own copy of the theme dict,
or a literal `#RRGGBB`. A name the theme does not carry is refused and says so, so a
misspelling is a message and not a colour.
`write` is the exception and the reason is in its signature: it is handed a box and a
string and no theme, so it takes the colour itself — `colour=theme["muted"]`,
`colour=theme["accent_ink"]`, or hex. `"muted"` there is six characters of a hex digit
and raises as one.
**A chart's `accent=` is not a colour.** It names *which item* to bring forward — a
category from the data you passed, `accent="Q3"` — and handing it `#RRGGBB` is refused
with the list of items it accepts.
A chart's colours are `chart_series`, and that is yours to set. Its order is the
contract — `chart_series[0]` is the series the page is about — so a comparison reads
by putting your own two colours in it: for the deck, `ppt_template(palette=
{"chart_series": ["#0B3D91", "#E4572E"]})`; for one page, copy the theme and set it
there before the call. A chart drawn from a theme you did not touch uses the
template's own series, which is the right default and not a rule.
**A card chooses its own ink.** `card` and `heading` set their type in whichever of the
theme's inks reads on the plane they were given, so `card(..., tint="accent")` on a
deep blue comes out with white type and the same call on a pale tint comes out dark.
You do not compute that, and you should not work around it: a `write` you place over a
plane yourself is yours to colour, and `theme["background"]` is the ink on a saturated
one.
**`accent` fills; `accent_ink` writes — on the page's own ground.** `accent_soft` is
the accent mixed towards the background, so a pale accent cannot be read on its own
tint. The theme carries `accent_ink` for this: the same hue, dark enough to write with
on the ground and on `accent_soft`, or the accent itself where that already reads.
Fills, markers and bars take `accent`; a number or heading in the accent's colour takes
`accent_ink`, by that name or as `theme["accent_ink"]`. On a plane of `accent` or a
page whose ground is saturated, `accent_ink` is a dark on a dark — a deep red heading
on a red page measured 1.09:1 — so type there is `theme["background"]`, the light ink.
**A paragraph set in one weight and one grey says nothing is more important than
anything else.** A reader scanning a slide takes the emphasised words first and reads
the rest only if those earned it, so every body paragraph that carries a number, a name
or a verdict marks that part and leaves the rest alone. What gets marked is the load:
the quantity and its unit, the one term the sentence turns on, the word that makes a
comparison a comparison. What does not is a phrase the author found interesting, and a
paragraph with four marked words has marked none.
Two marks and no third. **`accent_ink`, for the thing the page is about** -- one per
paragraph, at most two or three on a page, and if everything is in the accent the accent
has stopped meaning anything. **Bold, for a term the reader has to hold on to** -- a
label, a defined name, the head of a list item. Not italics, not underline, not a third
colour: a deck reads as one document when its emphasis has one vocabulary. A number that
carries the whole page is not emphasis at all -- it is the page's anchor, and it goes up
the size ladder as well (`no_anchor`).
`Run` is how a line says it, and §4 has the call. Where the whole block is the
emphasis -- a lead line, a pull quote, a card's heading -- `write`'s own `bold` and
`colour` are simpler and say the same thing.
**One font family, and its CJK companion.** Inside a template, the face `ppt_theme`
hands you is the template's own, whatever it is (微软雅黑, say), and that is the right
one to use: it is the house style, and the width measurement degrades to an estimate
for it rather than failing. Anything else is substituted on export and every position
you computed is wrong.
For a deck in Chinese that is only half the answer. `font_family` is a Latin face,
and Han characters set in one fall back to whatever the viewer has — a different
design at a different weight, and on the review renderer no Han glyphs at all. Every
theme carries `cjk_font_family` beside it — the template's own Han face where it names
one, and otherwise a face matched to the Latin one's class (`Noto Serif CJK SC` for a
serif, `Noto Sans CJK SC` for a sans) — and both names go on
the same run: `write(..., font=FONT, cjk_font=HAN)` from `ppt_layout` does it, or
set `a:ea` yourself. Then 目标查询 is set on purpose and `TarViS` in the same line
stays in the Latin face.
**A type scale with a big step at the top and fine steps below.** Same size for the
same role on every page. A 16.5pt title over 16pt body is not a scale; it is no
hierarchy at all. Two numbers in it are measured rather than chosen, because a deck is
read projected: body copy stays at or above **14pt**, and a caption, a source line or
any other short label may go to **10.8pt** but no further.
**A dominant element takes the top of the scale, and the top of it is above the title.**
A page whose point is one number sets that number at `NUMBER_PT`, the ramp's largest step
-- `the_largest_step_this_copy_takes(..., largest=NUMBER_PT)` is how to ask for it (§4) --
and it outranks the page title rather than sitting under it: a number set smaller than
the heading over it is not a dominant number, it is a caption on one. The inversion runs
the other way too, because copy is copy -- a concluding sentence set larger than its own
title makes the title the subordinate line. Where the size a page seems to want falls
between two steps, take the step above rather than a size off the ramp (§12).
Which floor a box answers to is decided from the box, not from you:
| The box | Floor |
| --- | --- |
| in the bottom 0.9in of the page -- furniture nobody reads from a seat | **8pt** |
| twenty characters or more, above that band -- copy | **14pt** |
| opening with `来源`, `注:`, `图`, `表`, `source:`, `figure` or `table` -- a caption in either language | **10.8** |
| shorter than twenty characters -- a label | **10.8** |
| fewer than four characters -- a page number or a chart tick | not measured |
The size measured is the one the *render* came out at, not the one you declared, so a
box too small for its copy is caught where autofit shrank it. Copy that will not fit
above the floor is a page with too much on it, and §12 says what to do instead of
shrinking it.
**One set of spacings, one surface treatment, one rule weight.** Fix them in the
setup beside the palette and call them everywhere: a larger gap between groups and a
smaller one inside a group, so grouping is legible from the spacing alone; one
restrained tint that planes are painted with; one thickness a hairline is drawn at.
Spacing that changes page to page reads as a deck assembled rather than designed, and
two groups with no more air between them than inside them come back as
`unseparated_blocks`.
**The header is the same on every page.** It is the one thing a reader sees eighteen
times, so the deck picks one header form and keeps it: the same kicker, the same
title position, the same treatment. Which form is yours, within the one the template
already has: its own title row, measured for you (§8). What a deck may not have is
three of them.
**Avoid -- the treatments that read as generated.** Each is a default reached for
instead of a decision, and each is legible as one from the back of the room:
- **A decorative rule or accent line under a title.** The title's own step in the scale
already separates it from the page; a line drawn under it adds a horizon that cuts the
heading off what it heads, and it appears on every page because it was never chosen
for any of them. `heading` draws none, and `rule` caps at 1.05in so it cannot become
one -- it is a short mark under a heading or beside a number. A page that genuinely
wants a divider gets a hairline under 4.5pt, or a thin `plane` for a full-width one.
- **A filled shape whose content is the space it covers.** A colour bar across the page,
a saturated strip down the edge of a card, a tinted panel painted over whatever room
was left with one sentence centred in it: each is a shape carrying no information,
which is what makes it read as filler -- a reader sees a region announced and finds
nothing in it, and the emptiness the shape was covering is now emphasised instead.
Grow the load-bearing content or redistribute it (§3.5). One filled bar is the deck's
own, the band the title row sits on; `band` (§12) says which shapes and at what size,
and `excessive_whitespace` measures what the panel was hiding.
The call is `card_size`, and a region is not a height: `card_size(w, icon=, title=,
body=)` answers how tall one card's own content makes it, so a row of them is
`max(card_size(...).h for each)` and the row is drawn at that. Take only that much of
the region and leave the rest to the next band -- a card handed `cells[i]` straight
out of `grid` is as tall as the region however little it holds, which is the void.
Cards take an `icon` for the same reason: three lines under a mark fill a card that
three lines alone leave two thirds empty, and the mark is what makes the row scannable
rather than three paragraphs side by side.
**The bar is usually not a mistake -- it is an answer to a page that felt empty**, and
wanting a full page is right. A stretched shape is the wrong way to get one, because it
fills the pixels and not the page: the reader still finds three short lines in a
half-page region, only now with a colour announcing it. What fills a page is content,
and the moves are these, in this order -- say more on the page (a line of detail under
each card title, the figure the number came from, the units, the year); give each card
an `icon`; use fewer, wider cards so the copy reaches their edges; put the figure or
the table in the room the cards left; or move a point here from a page that has one too
many. Only when none of those is true is the page genuinely short, and then the answer
is one page fewer, not one bar more. If you have already drawn the bar, the check is
the same either way: cover the shape's fill with your hand and ask whether the page
still says as much. If it does, the fill was doing nothing.
- **A title too close in size to its body copy.** The step at the top of the ramp is
what makes a title a title, and two sizes a point apart are one size to a reader.
`type_scale` and `type_drift` (§12) read the sizes back off the render.
- **Type too small to read projected.** The floors above are the measured half of this;
the other half is the page shrunk to a third of its size (§10).
- **The same page skeleton with only the words changed.** The header repeats on purpose
and nothing under it does: a cover, an agenda, a two-way comparison, a table page, a
full-bleed figure and a closing page are each recognisably a different kind of page.
`layout_variety` (§12) reads this off the shapes each page actually carries.
- **A filled chip behind every icon.** A row of identical coloured badges is the tell.
An icon is a mark beside type (§7); where a block wants a surface, give the whole
block one.
## 3.5 Structure follows the content
Decide what carries the point before placing anything.
**Every content page carries a visual element** -- a source figure, a chart you drew, a
drawn table, a diagram, or a region led by icons. A title over a column of copy is the
page that comes out when nothing was decided. The deck-wide count, its threshold and what
counts are `evidence`'s (§12); the decision here is which element this page's claim is
made of.
**Reference — not a constraint.** The table below is vocabulary, not a lookup. A page
may take one row, several rows together, or a composition no row names; no row carries
a coverage quota and nothing counts which ones a deck used.
| Content logic | What carries it |
|---|---|
| Real product, interface, experiment or published plot | a source figure |
| Source-backed numeric comparison or trend | a chart you draw |
| Tabular data or comparable rows and columns -- figures a reader compares down a column | a drawn table or comparison matrix; not for four labelled points, which are cards or a list |
| Familiar capabilities or categories | icon-led regions |
| Sequential steps or milestones | a timeline, a numbered column, or stages as unequal blocks; §7.5 |
| Two genuinely contrasted alternatives | paired regions on one baseline |
| One memorable conclusion | a single dominant statement |
| One dominant number | the number at display size, reasoning beside it |
Then compose around it. Give the load-bearing element the space its role deserves
and let the rest defer; a page where every region carries equal weight has argued
nothing. Vary composition because the content varies, not to fill a quota, and keep
a series of comparable cases on one shape so the reader can compare them. Give a
conclusion or decision its own baseline, weight or restrained accent instead of styling
every component identically.
Prefer one coherent composition over a dashboard of unrelated panels. Do not draw boxes
merely to avoid a prose page.
**Three blocks of copy in one body need something drawn between them.** Two read as a
pair from their positions alone -- a claim and its qualifier, a before and an after.
Three stop doing that, and their grouping becomes a grid the reader has to infer from
x-offsets. One device is the whole floor and any of them counts: `card_group` for blocks
that are the same kind of thing, `plane` behind the one region that answers the page,
`rule` between two halves that are not cards, or a figure the copy is set against.
A plane under a picture takes the page's own ground -- on a dark page a dark surface,
never a pale one -- and a picture that carries its own ground goes on the page without one.
Measured over the 105 content pages the bundled templates ship: 104 draw at least one,
the median page draws seven, and every page that draws exactly one draws a picture.
`undivided_body` (§12) reports the page that draws none. This is not the previous
paragraph's box-for-its-own-sake -- it is the difference between three groups a reader
can see and three paragraphs a reader has to sort.
**With a template bound, a page takes its shape from an example page first** (§8): the
outline names the example each content page starts from, and composing is for the page no
example or borrowable page can carry. **A composed page takes its shape from the catalogue.** Open
[deck/build/references/layouts.md](deck/build/references/layouts.md) while deciding. Its
Part 1 is eleven skeletons -- every id folded into the one it varies, with the share of
the body each division measured on the pages that were drawn -- and the outline names
what you chose: `layout` takes the one structure id, `layers` the modifiers stacked on
it, and `anti_pattern` the way this page would go wrong. A page cloned from a template
example leaves `layout` empty, because its structure is the example's.
**Every band of the page is declared, and the hierarchy is legible.** A composed page
divides into regions before it divides into shapes: count them, and give each the layer
that says what it is (`layers`), because the band nobody planned is the one that comes
back as bare paragraphs.
Then rank them so the reader sees it before reading a word -- which region is the claim,
which supports it, which is aside.
Cards, planes and helper presets are primitives, not a required page grammar. Do not wrap
every component in a filled rectangle just to make its boundary visible; use hierarchy,
alignment, whitespace and meaningful rules first.
**Fill the page.** A 13.3 x 7.5in canvas holds far more than a title and four
bullets, and content that stops two thirds of the way down is the most common
failure there is. Whitespace you chose is a margin around content that fills the
frame; whitespace left over reads as unfinished. When a page comes back with a large
accidental empty field, grow the load-bearing content, introduce a second visual
layer, or redistribute what is already there -- never decoration, restated copy or
smaller type, each of which makes the page worse than the gap did.
When the source already carries a real table, comparable rows or a set of parallel items
with several attributes, that information shape is part of the evidence: do not flatten
it because the text happens to fit.
On a figure-and-text page, let the figure dominate and turn the supporting copy into
scan points with clear labels -- as many as the region beside the figure actually
holds, which `points_size` answers before the page is drawn rather than after (§4).
Put those points on one or more restrained theme-coloured surfaces so the image and
explanation read as distinct layers; do not leave a bare paragraph floating beside the
figure. The cards defer to the figure -- they are supporting structure, not a row of
equal dashboard tiles.
**The caption goes in the page's foot, not under the figure.** A line of small type
under every picture puts a second row of furniture in the middle of the body, and on a
page with two figures it puts two -- the reader's eye leaves the figure, reads a label,
and comes back. Collect what the figures need into `footer()`'s `note`, joined with
`;`: `footer(slide, frame.footer, T, note="来源:xxx;图 1 主街三段式动线")`. Where the note
will not fit, keep the source and the one caption that carries a fact, and drop the rest
-- never shrink the type to fit it in; `footer` measures the note and refuses a second
line rather than drawing it into the margin. A number or a label *on* the figure is not
a caption and stays where it is, and so is the label that says *which* cell of a small
multiple this is: five identically framed figures are compared by their labels, and
moving those to the foot leaves the reader counting cells.
It is measured, not only a preference. A caption strip under a figure at the foot of the
body puts its own top edge inside the band `footer_findings` reads as the page's foot --
on five reference pages, three of them at 6.40in and 6.44in. Three fifths is over the
share that reading takes as a habit, so the deck was judged to have feet, and then
reported as `unnumbered_pages` because a caption carries no `slidenum`. A caption under a
figure manufactures a false foot and a false warning at once.
**Asking for the foot costs more than the caption frees.** `page(footer=True)` takes
`_FOOTER_H + GUTTER` = 0.58in off the body -- 5.25in becomes 4.67in -- and a caption
strip is about 0.40in. So a page that gains a foot has a *smaller* body than before, by
about 0.18in, and its bands have to be measured again: two reference passages were laid
out against 5.25in and `take` refused them at the band where the room ran out. Re-measure
with `short_by` before drawing, and where a band has to go, cut a heading whose cards
already name themselves rather than the content.
Every placed figure is still accounted for, and a figure can carry two claims
that are not the same kind. Its **source caption** is what the source printed under it:
quote it, credit it. The **`visual_caption`** `ppt_figure_inspect` wrote is a description
of the pixels and nobody's caption: write your own line from it, never present it as the
source's words, and never put a name in front of it the materials do not establish -- a
figure credited to the product it happens to sit beside is the failure this exists to
stop. A caption says what is shown, not a claim the pixels do not prove.
## 4. The program
It goes at `deck/build/build.py` — that whole path, relative to the
workspace; `ppt_prepare` reports it as `write_the_program_to`. A bare
`build/build.py` lands somewhere the build does not look. `write_file` creates it,
`write_file` with `mode="append"` extends it, `edit_file` revises it. Never restate
the whole file to change part of it.
**Write it in pieces, not in one call -- and in few pieces.** `write_file` for the setup
and the first five pages, then `mode="append"` for five pages at a time, with
`ppt_build(draft=true)` in the same reply as each append -- the calls in one reply run in
the order you list them, so the write and the build together are one turn: a draft builds
what exists, measures it and hands back the renders without holding a part-written deck to
the agreed length and without publishing. Four appends and four drafts is a twenty-page deck.
Use `write_file`, not `exec`, to write the program. Drop `draft` when the deck is whole and you want the gates. A twenty-page
program sent in one call is cut before it arrives, and a draft shows you a page while
there are three rather than twenty.
It runs in the build directory with python-pptx and Pillow and reads its paths from the
environment: `PPT_OUTPUT` (save there and nowhere else); `PPT_FIGURES_DIR` (the figures
ingest extracted, and the only way to one — take
`FIGURES = os.environ["PPT_FIGURES_DIR"]` in the setup and write `f"{FIGURES}/fig2.png"`
wherever a page places a figure, with the name the figure list gives after `file` — the id
before it is the catalogue's key, not a file; there is no `figures` directory under `deck/build`);
and, with a template bound, two paths that are not interchangeable — `PPT_TEMPLATE` is
the template with its example pages **removed**, the deck you build into
(`prs = Presentation(os.environ['PPT_TEMPLATE'])`), and `PPT_TEMPLATE_SOURCE` is the
user's original with those pages still in it, the only thing `prototype` can read a page
out of (`tpl = Presentation(os.environ['PPT_TEMPLATE_SOURCE'])`). Handing `prototype` the
deck you build into raises `this template ships 0 pages`.
`PPT_BUNDLED_TEMPLATES` is the folder the bundled templates ship in; `bundled(name)` reads it,
so a page borrowed from another template (§8) needs no path in the program.
`PPT_SLIDE_BLOCKS` and `PPT_SLIDE_LINES` are the build's own bookkeeping -- the `# SLIDE n`
spans it runs one at a time (§4) and the record of which line drew which page -- and
nothing in the program reads them.
**One block per page, opened with a `# SLIDE <n>` banner.** Shared helpers above the
blocks; the page's own composition inside the page's own block. Two shapes are refused: a
`build.py` that runs another file, and a loop that draws every page from one call site.
Both leave no code belonging to one page, so nothing can match a render to the code that
drew it, report a defect against the block that caused it, or fix one page without
touching another.
**The whole program, ending included.** Every other example here and in the references is
a page fragment; this is the file they go inside, and its last line is the one that gets
left out. A program that draws twenty pages and never saves them leaves no deck.
```python
import os
from pptx import Presentation
from ppt_layout import BODY_PT, heading, page, points, write
from ppt_theme import THEMES, rgb
prs = Presentation(os.environ["PPT_TEMPLATE"]) # the template, its example pages gone
T = THEMES[next(iter(THEMES))] # one entry, and it is the template's
FACE, HAN = T["font_family"], T["cjk_font_family"]
INK = rgb(T["foreground"])
HOUSE = "Title Only" # house_style's layout_for_a_page_you_draw
LAYOUT = next(one for one in prs.slide_layouts if one.name == HOUSE)
def content_page(): # the one skeleton every page shares
return prs.slides.add_slide(LAYOUT), page() # the template's background comes with it
# SLIDE 1
slide, frame = content_page()
heading(slide, frame, T, "四类任务,一套权重", "01 结论", font=FACE, cjk_font=HAN)
write(slide, frame.body, "切任务只换输入查询,不重训。", size=BODY_PT, colour=INK,
font=FACE, cjk_font=HAN)
# SLIDE 2
slide, frame = content_page()
heading(slide, frame, T, "证据", "02 消融", font=FACE, cjk_font=HAN)
points(slide, frame.body, T, ["去掉时序颈:44.7 对 46.3", "去掉语义查询:不收敛"],
font=FACE, cjk_font=HAN)
prs.save(os.environ["PPT_OUTPUT"]) # the last line of every program
```
**`prs.save(os.environ["PPT_OUTPUT"])` is the last line of every program**, draft builds
included. Nothing saves for you and nothing else is the place to save to.
Then write the helpers *your* pages need. Beyond the header, the source line and the
page number, page types have no reason to share a skeleton (§3).
**Default — a `*` after the geometry in every helper you write.**
`def card(slide, x, y, w, h, *, fill=SOFT, bold=False, align="left")` — the boxes stay
positional because they are always the same four numbers in the same order, and
everything after them is named at the call site. Seven or eight positional parameters is
the largest single cause of a build that will not run at all: an empty string where a bold
flag goes, an argument short, a keyword the function never had. `ppt_layout`'s own helpers
are shaped this way.
**The agenda is the page that goes wrong most often.** It is a map of the argument:
the movements the talk makes, each named with one line saying what it settles.
*Default — five to seven of them*, and nothing counts them; the outline's own sections
usually run to eight to twelve, so a deck may well want more. What is not a default:
it is **not an index of slide numbers**. "3 Introduction, 4 Limitations, 5 Overview…"
enumerates the file rather than the talk, and tells an audience nothing the page numbers
do not.
**Ask how wide the text is instead of guessing at it.** The commonest geometry bug in a
generated deck: a box sized to what the string looks like wraps it onto a second line, the
second line pushes into whatever sits below, and the page reads as broken rather than as
tight. `text_size` is the width and height the copy really takes and `fits` is the
yes-or-no, both answered before anything is placed. Where a line must not break whatever
happens -- a number, a label, a kicker, a card heading -- say so on the frame `write` hands
back: `write(...).shape.word_wrap = False`, where `.shape` is the text frame itself. Extra
width costs nothing; an unfilled text box has no fill and no outline.
**`print()` is how those answers reach you.** Whatever the program writes to stdout comes
back in the build reply, on a failed build as well as one that worked -- so `text_size`,
`fits`, `formula_type_size`, `len(units(slide))` and the theme your script actually
resolved are one `print` from being visible. Print the measurement, not the whole theme:
the last 20,000 characters are what comes back. It is also the only way to find a page
that runs and draws the wrong thing.
The agenda's geometry breaks the same three ways every time, so compute it:
- **A two-digit number needs a box that fits two digits.** `01` and `10` fold into two
stacked characters in a box sized for `1`. Set `write(...).shape.word_wrap = False`, or
give the box the width the widest label needs.
- **Lay repeated rows on a pitch.** With `n` rows in height `H` the pitch is `H / n`, the
label sits at `y = top + i * pitch`, its description at a fixed offset below.
- **A label and its description are one block.** If the description can run to two lines,
the pitch has to allow for two, or row `i + 1` lands on top of it.
The same three apply to any page built from repeated rows or cards.
### The helpers, by signature
Everything below is already beside your script: what each helper takes, what it hands
back, and what it can be asked before it draws. Read this rather than the modules (51k
tokens of source, in context for the rest of the run), and a helper that is neither here
nor in a reference this page links is one you should not be calling. The drawing base
`ppt_charts` is built on is left out below and is in §6.
Import them by name. What follows is the whole set this page and its references call, so
a snippet copied from either has its import here; take the lines the page you are drawing
needs, and an import you never call costs nothing. A name used in an example and absent
from every import line is a `NameError` at build time, so the import travels with the snippet.
```python
from ppt_theme import THEMES, rgb
from ppt_layout import (
BODY_PT, Box, CANVAS_H, CANVAS_W, Drawn, Frame, GUTTER, Inches, KICKER_PT, LABEL_PT,
LEAD_PT, MARGIN, MSO_SHAPE, NUMBER_PT, PAD, Pt, Run, TITLE_PT, card, card_body_box,
card_size, fits, footer, formula, heading, mark, page, picture_fit, picture_size, plane,
points, rule, stack, table, table_size, text_size, the_largest_step_this_copy_takes,
what_this_page_gave_up, write
)
from ppt_shapes import connect, preset, timeline
from ppt_icons import add_icon, find_icons, the_ink_an_icon_covers
from ppt_charts import (
TooSmall, column, contrast, fmt, hline, horizontal_bar, ink_on, key, line_height, linear,
number, pick_size, poly, rect, shades, span, text_width, type_face, vline, waterfall,
write_label
)
from ppt_template import (
add_unit, backdrop, bundled, clear_region, clone_page, clone_shape, drop_shape,
layout_pictures, page_box, prototype, raise_type, remove_unit, replace_picture, replace_text,
shape_at, shape_near, shape_saying, shapes_in, units, wash
)
```
Every `deck/build/references/...` link on this page is a file in the build directory,
beside the modules it documents, and that whole path is what `read_file` takes -- the file
tools resolve against the workspace, not the directory your program runs in. A table drawn
without reading its reference gets the bare default.
`ppt_layout`, always present:
| | |
| --- | --- |
| `page(kicker=True, footer=False)` | → `Frame(kicker, title, body, footer)`, four boxes inside the safe area; ask for the footer on a page that cites or carries a page number, and the body gives up the strip for it |
| `footer(slide, box, theme, *, note=None, number=True, colour=None, font=None, cjk_font=None)` | draws that strip: a hairline across it, `note` on the left, and the page number on the right as a real `slidenum` field. **`note` is one line** -- the strip is the page's bottom edge and a second line goes into the margin, so a note too long for the lane (the strip less 1.6in for the number) comes back as this call refusing, with the width it had and the width it needed. Every bundled template defines a page-number placeholder on its master and puts one on no page, and python-pptx does not clone a footer placeholder onto a slide — so a deck gets page numbers only if the script draws them |
| `Frame(kicker, title, body, footer)` | the same four boxes as a value you can build. `page()` is the ordinary page and not the only one: a frame you make yourself is what a left rail, a full-bleed opener or a title over two thirds of the canvas is made of, and every helper that takes a frame takes yours without knowing the difference |
| `frame.laying(*heights, gutter=GUTTER)` | **the default way to lay a content page's body.** A cursor down the body with the leftover already spent between the bands, so the last band ends on the body's own floor. Pass the bands, take them back in the same order, and **no `skip` between them** -- the gaps are what the leftover became. Two bands or more; one band has no gap to spend and comes back centred, which is `holding`'s case |
| `frame.holding(*heights)` | the same frame with its body cut to the run these heights add up to, and the leftover split as air above and below that run instead of a band of white along the page's foot. The bands and the gaps between them, in the order they occur; a list works too. For the path where the page measures its bands and takes them off a cursor -- **not** for a run something else already spreads into the whole body (`card_group(..., down=True)`), whose cards would then touch. Only the body moves, so it can be asked before or after `heading`, and a run with no slack comes back unchanged |
| `Box.corners(x0, y0, x1, y1)` | a box from its **two corners** |
| `Box.at(x, y, w=, h=)` | a box from a corner and a **size**; the size is keyword-only. It reads back under the same four names (`box.x`, `box.y`, `box.w`, `box.h`) as well as `box.x0..y1` |
| `box.rows(n, gutter=GUTTER, weights=None)`, `box.columns(...)` | n boxes filling this one |
| `box.grid(cols, rows, gutter=GUTTER)` | row-major cells |
| `box.split_left(fraction, gutter=GUTTER)`, `box.split_top(...)` | two boxes, the first taking `fraction` |
| `box.inset(dx=PAD, dy=None)` | a smaller box inside this one |
| `stack(box, gutter=0)` | a cursor down a region: `.take(height)`, `.rest()`, `.skip(height)`, `.left`, `.short_by(*heights)`, `.spread(*heights)`, `.centre(*heights)`; bands are adjacent, `skip` is the gap |
| `picture_fit(slide, image, box, theme, *, caption=None, size=LABEL_PT, align="center", font=None, cjk_font=None)` | a picture scaled to fit the box whole, centred. `caption` draws a strip under it and **the deck's captions do not go there** -- they go in `footer(note=)`; it is for the label that says which cell of a small multiple this is |
| `heading(slide, frame, theme, title, kicker=None, *, tint="surface", bleed=True, size=TITLE_PT, anchor="middle", font=None, cjk_font=None)` | §6.5 — `anchor` is the template's, not this default; §8 |
| `write(slide, box, text, *, size=BODY_PT, colour="#000000", font=None, cjk_font=None, bold=False, align="left", anchor="top", spacing=1.15)` | `text` may be a list of paragraphs |
| `points(slide, box, theme, items, *, size=BODY_PT, numbered=False, mark="•", colour=None, font=None, cjk_font=None, mark_colour=None, spacing=1.25)` | §6.5 — it takes the theme, so the faces are optional here |
| `card(slide, box, theme, *, icon=None, title="", body=(), tint="surface", size=BODY_PT, title_size=LEAD_PT, font=None, cjk_font=None)` | §7 |
| `card_group(slide, box, theme, items, *, down=False, gutter=GUTTER)` | a row of cards across the region, or a column down it with `down=True`. Each item is a dict of `card`'s own arguments, so every field it carries reaches the card; a row is levelled with `card_size` and centred, a column keeps each card's own height and `spread`s the leftover. §7 |
| `formula(slide, box, text, theme, *, size=BODY_PT, align="left", anchor="top", font=None, cjk_font=None)` | §9 |
| `plane(slide, box, theme, tint="surface", radius=True, opacity=1.0)` | a painted region, rounded. **A group of copy that sits on the page's own ground has no edge, and a reader cannot see where one group ends and the next begins** -- three columns of body copy under a band came back as "the components have no boundaries". Put every group that is not page furniture on a plane; `radius=False` only for a band running to the trim |
| `rule(slide, box, theme, thickness=0.03, colour=None)` | a hairline **0.06in below** the box and **at most 1.05in long** — a short mark under a heading or beside a number, not a divider across a region. For a full-width line draw a thin `plane`, or take the box a chart hands back |
| `table(slide, box, rows, theme, *, weights=None, size=LABEL_PT, numeric_from=None, style="header_tint", emphasize_rows=(), emphasize_columns=(), group_rows=None, indent_rows=(), total_rows=(), marks=None, header_size=None, align=None, rule_pt=None, grid_pt=None, row_height=None, header_height=None, padding=None, fill=True, column_rules=True, banding=False, fills=None)` | §6 |
| `Run(text, size=None, bold=None, colour=None)` | one stretch of a line with its own size, weight or colour — a paragraph handed to `write` may be a list of these instead of a string |
| `mark(slide, box, theme, kind, value=None, *, colour=None)` | [deck/build/references/tables.md](deck/build/references/tables.md) |
| `overlaps(boxes, tolerance=0.01)` | → the `(i, j)` pairs that overlap |
Constants: `CANVAS_W`, `CANVAS_H`, `MARGIN`, `GUTTER`, `PAD`; the ramp `TITLE_PT`,
`LEAD_PT`, `BODY_PT`, `LABEL_PT`, `KICKER_PT`, `NUMBER_PT`, `BODY_FLOOR_PT`.
### What comes back, and how to ask before you draw
Every helper that *draws* hands back a `Drawn` — the five that compute instead hand
back what they computed: `page()` a `Frame`, `stack()` a `Stack`, `overlaps()` a list
of `(i, j)` pairs, a `Box` divider a `Box` or a list of them, and `mark()` a `Marks`,
which is a list of the shapes it made carrying `.box` for the ink they actually cover
— the one measurement that says whether a marked column is wide enough to read. A
`card_group` reads the same way: the cards' own `Drawn`s in order, carrying `.box` for
the run as a whole.
For the drawing ones: `.shape` is the python-pptx object it made and `.box` is **what it
actually covered**, not the box you passed in -- a rule sits below the box it underlines, a
picture keeps its own aspect, and copy that did not fit comes back taller than its box. An
attribute the tuple does not carry is looked for on the shape and then on the box, so
`write(...).paragraphs`, `table(...).columns` and `heading(...).x1` read straight through.
Place the next thing on the page off the last thing's `.box`, never off the number you
chose for it; `overlaps` takes those boxes and names the pairs that collide.
And every one of them can be asked before it draws, which is what turns a render
round into an `if`:
| | |
| --- | --- |
| `lines_needed(text, width, *, size=BODY_PT, font=None, bold=False)` | how many lines this copy wraps onto at that width |
| `text_size(text, width, *, size=BODY_PT, font=None, bold=False, spacing=1.15)` | the box the copy really needs, at the origin: `.h` is what to ask a stack for, `.w` is what the longest line actually sets |
| `points_size(items, width, *, size=BODY_PT, font=None, spacing=1.25)` | the same for a bulleted list, whose hanging mark and paragraph spacing `text_size` knows nothing about |
| `fits(what, box, *, size=BODY_PT, font=None, bold=False, spacing=1.15)` | yes or no. `what` is copy, or any box one of these handed back |
| `table_size(rows, theme, *, weights=None, size=LABEL_PT, style="header_tint", numeric_from=None, group_rows=None, marks=None, header_size=None, indent_rows=(), row_height=None, header_height=None, padding=None, fill=True, box=None)` | where the table ends, before a cell of it is drawn. With `box` it is placed at that box's corner and held to its width |
| `picture_size(image, box, *, caption=None, size=LABEL_PT)` | the room the figure needs inside `box`, off the image's own pixels. Pass `caption` only where `picture_fit` is going to draw one, or the reservation is for a strip nothing fills |
| `formula_type_size(text, width, *, size=BODY_PT, font=None)` | the size `formula` will really set it at; one that comes back at `BODY_FLOOR_PT` wants a wider column, not another build |
| `the_largest_step_this_copy_takes(text, box, *, font=None, bold=False, spacing=1.15, wrap=False, largest=TITLE_PT)` | the biggest step of the ramp the copy still fits that box at — the only call that answers upwards. `wrap=False` keeps it on the lines you gave it; `wrap=True` is for copy meant to reflow. `largest` is the step to stop at and you name it: `TITLE_PT` for a label in a shape, `LEAD_PT` for a line that leads a band, `BODY_PT` for copy, `NUMBER_PT` when the copy is the figure. It is not inferred — a character count cannot tell a long word from a sentence |
| `what_this_page_gave_up()` | every concession the measuring calls made on this page since you last asked, and it clears the list as it answers. A ramp walk that reached `BODY_FLOOR_PT` and a formula that broke at a separator both come back here: they returned a size rather than raising, so the page looks measured and the copy was quietly cut down to fit. Call it once a page is drawn and read what it says before the build does |
| `card_body_box(box, *, icon=None, title="", title_size=LEAD_PT)` | where a card's copy starts, once the icon and the title have taken their line |
| `card_size(width, *, icon=None, title="", body=(), size=BODY_PT, title_size=LEAD_PT, font=None)` | how tall a card has to be for what goes in it — `max(card_size(w, **c).h for c in cards)` levels a row without padding it out to the page, which is what `card_group` does for you; reach for this on its own for a group that is not a plain row or column |
| `stack(box).room` | what is still unspoken for, as a box, **without taking it** — `fits(picture_size(fig, down.room), down.room)` is the whole question |
| `stack(box).short_by(*heights)` | how many inches the whole plan runs over, or 0.0 — measure every band, ask this, **then** draw. `rest()` spends the region it answers with; `room` is the same box and does not |
| `stack(box).slack(*heights)` | the same arithmetic the other way: how many inches the region has left once these bands are in it |
| `stack(box).centre(*heights)` | half that leftover above the run, half below, and hands the cursor back to chain. For a single run that sits against something beside it |
| `stack(box).spread(*heights)` | the leftover becomes the gaps *between* the bands, so the run ends on the region's bottom edge. **A lane of cards, and two columns that have to come out level.** Never less than `GUTTER` between them, so a run with no leftover overruns instead of welding — count the n-1 gaps into `short_by` and shorten the bands. No `skip` after it, which overruns by exactly what was skipped |
**A short label in a big box: ask, do not name a step.** Every call above except the last
measures downwards. What that leaves out is the label its box is far too big for: naming
the smallest step of the ramp puts a stage label at 14pt in a shape 1.25in tall. Two or
three characters in a box over an inch tall want
`the_largest_step_this_copy_takes(label, one.box, font=F)`. Everything playing one role on
the page shares one size, so ask for each and take the `min`.
**Ask `lines_needed` before you fix a band's height.** Copy that runs long does not
shrink (§8) -- it runs out of its box and over whatever is under it.
`lines_needed(title, frame.title.w, size=TITLE_PT)` coming back 2 is the row below moving
down or the box getting wider, decided before anything is drawn.
`ppt_charts` answers the same kind of question about a chart, by running it against a
slide that draws nothing:
| | |
| --- | --- |
| `the_smallest_box_a_chart_needs(chart, theme, *data, **knobs)` | the smallest box this chart takes **this** data in, as a `Box` at the origin |
| `whether_a_chart_fits(chart, box, theme, *data, **knobs)` | whether it would take the box you have at all |
| `what_a_chart_will_do(chart, box, theme, *data, **knobs)` | the `Drawn` a real draw would return, with nothing written |
A chart's `Drawn` **is** a `Box` — the plot itself, what is left once the category labels,
the axis readings and the key have taken theirs — and it carries `where` (the chart's own
scale, so a rule at 80% goes where the chart put 80%), `names_not_written` and
`readings_not_written` (what it dropped for want of room), `marks_not_to_scale` (bubbles
drawn at the floor instead of to area) and `type_pt` (the step of the ramp its labels
landed on — a reading, never a setting). `nothing_was_dropped` is those three in one
boolean.
A chart that will not take its box raises `TooSmall`, carrying `short`: the deficit in
inches as `(across, down)`. **It raises before it draws anything**, so a `try/except`
around one has no wreckage to clear -- and `whether_a_chart_fits` is that `try/except`
already written.
`ppt_charts`, always present — twenty-three charts drawn as shapes. Each takes the slide,
a `Box` out of the grid, the theme, and its data; each fills the box, and there is no
size, face or colour to pass: type comes off the ramp above and steps down when a label
does not fit, colour comes off the theme. Signatures and the data shape each one reads:
[deck/build/references/charts.md](deck/build/references/charts.md) (§6).
`ppt_shapes`, always present — the 109 Office preset geometries a business page can
use, by their DrawingML names, and the two layouts built on them. Signatures in
[deck/build/references/shapes.md](deck/build/references/shapes.md), with §7.5.
`ppt_template`, only when a template is bound:
| | |
| --- | --- |
| `prototype(template, number)` | the template's page `number`, counting from 1 |
| `bundled(name)` | another bundled template by file name, for a page borrowed from it (§8); found through `PPT_BUNDLED_TEMPLATES`, which `ppt_build` sets |
| `clone_page(presentation, prototype)` | **copy a template page into the deck, its words and all -- the one way a template page is put to work**; §8 |
| `units(container)`, `arrangement(run)` | the page's repeating units; how a run is laid out |
| `boxes(run)` | each unit's `(left, top, width, height)` in inches, page order — a size, **not** a `ppt_layout.Box`, so `boxes(run)[0][2]` is a width and not a far edge |
| `place(unit, box)` | move one unit; the box is `(left, top, width, height)` in inches, and a `ppt_layout.Box` is accepted and converted from its two corners |
| `shape_at(slide, number)`, `drop_shape(shape)` | one shape by index, counting from 1; remove it |
| `shape_near(container, left, top, tol=0.08, with_text=False)` | one shape by where the page shows it, in inches |
| `shape_saying(container, prefix)` | one shape by the copy it starts with |
| `page_position(shape)` | `(left, top)` in inches on the page, groups resolved |
| `page_box(shape)` | where a shape is drawn, as the **two corners** a region is given in, with the scale its group applies -- so it drops straight into `clear_region` |
| `shapes_in(container, box, *, share=0.5, with_text=False)` | every shape drawn in `box` -- a `ppt_layout.Box` or a `page_box(shape)`, never four bare numbers -- groups walked into -- `shape_near`'s plural, and the way to find by place rather than by words; `share` is how much of a shape must be inside to count, and `share=0` asks what touches the box at all |
| `clear_region(container, box, *, share=0.5, keep=())` | empty `box` -- `Box.corners(...)`, `Box.at(...)` or `page_box(shape)`, since four bare numbers cannot say which reading they are and are refused -- so you can draw in it: `drop_shape` for everything `shapes_in` finds, returning what it removed and, in `left_standing`, what still lies over the box. `keep` spares a shape by the words it shows or by its `# [n]` |
| `raise_type(slide, floor=BODY_FLOOR_PT, min_chars=COPY_CHARS)` | lift copy the template states under the readability floor (`BODY_FLOOR_PT` is 14pt) and let its box grow to hold it |
| `replace_text(target, text, new=None)`, `replace_picture(shape, image, fit="contain", *, anchor="centre", trim=None, zoom=1.0, alpha=None, box=None)` | in place, keeping how the template set it; `alpha` washes the new picture the way `backdrop` does, for the frame that is the page. `shape` may be a drawing rather than a frame -- a group of freeforms, a cartoon -- or a list of shapes making one: the picture takes its box and its depth and the drawing goes |
| `backdrop(slide, image, *, alpha=1.0, scrim=0.62, ink=None, light_type=True, box=None, anchor="centre", trim=None, zoom=1.0)` | a photograph behind everything on the page, cover-cropped to the canvas (or `box`), at full strength under a plane of the theme's ink at `scrim`, with every run of type on the page set light (saturated accents kept) -- the cover form; `scrim=None` with `alpha` at 0.12 or under is the texture form behind a template's own ground; returns the picture |
| `layout_pictures(slide)` | the photographs a page inherits from its layout, largest first; `replace_picture(layout_pictures(slide)[0], image, "cover")` changes them for every page on that layout; one the size of the page is the page's background -- `alpha=0.1` keeps it the texture the template meant (0.12 to 0.80 is the fog `washed_backdrop` reports), and a photograph meant to be seen goes in at full strength under a plane of ink with light type, as `backdrop` lays them |
| `wash(shape, alpha)` | set any picture's transparency -- a frame the template drew, one `replace_picture` filled, one you placed -- to the same share `backdrop` takes; a picture at full strength again is `wash(shape, 1)`; a photograph a title has to read over wants a plane of ink over it, not a wash to 0.3, which reads as fog |
| `add_unit(target, count=1)` | one more slot on a page's repeating run -- a run from `units(slide)`, or the slide for its longest -- copied from the last unit and laid out again |
| `remove_unit(unit)` | one slot fewer, and the row closed up; `drop_shape` removes and leaves the hole |
| `clone_shape(shape, box=None)` | a copy of one shape on the same page, at a page box -- the `clone_panel` five builds wrote for themselves |
**One word in a line, in the accent.** `write`'s `size`, `bold` and `colour` style the
whole box, so a number that has to carry the page needed a second text box beside its
own unit. A paragraph may instead be a list of `Run`s, each with its own:
```python
write(slide, box, [[Run("营收 "), Run("1.4 亿元", size=16, bold=True, colour=ACCENT_INK)]],
size=14, colour=INK, font=F, cjk_font=HAN)
```
Every field but the text falls back to the call's own, the line is measured and its box
returned at its **tallest** run, and this is what `no_anchor` is asking for when it says
to set the number that carries the page two steps up the size ladder.
**A veil over a photograph, not a panel.** Copy set straight onto a picture is readable
where the picture is dark and gone where it is not, and `unreadable` reports the half it
can measure. A veil puts an even ground under the type and leaves the photograph
visible; 0.35 to 0.55 is the usual range, and an opaque plane over a picture is the
picture thrown away.
```python
picture_fit(slide, PHOTO, frame.body, T)
plane(slide, frame.body, T, tint="foreground", radius=False, opacity=0.45)
write(slide, frame.body.inset(0.6), claim, size=TITLE_PT, colour=T["background"], bold=True)
```
**Take a handle by what you know, and never by `shape.left`.** A template's content is
mostly inside groups, and a shape in a group states its position in the *group's*
coordinate space, scaled by the group's own extents — so `shape.left` is not where the
shape is, and comparing it against a coordinate read off the render finds nothing. Three
of four live authors wrote that comparison by hand and it is four of their twenty build
failures. `page_position(shape)` is the resolved position, `shape_near(slide, 1.56, 2.47)`
is the search over it, and `shape_saying(slide, "Method")` is the same search by copy;
each refuses by listing what the page does hold.
**A cloned page carries the template's type sizes.** Where those are under the
readability floor, `raise_type(slide)` lifts them -- run sizes, and the paragraph or
list-style size a bare run inherits -- and swaps the autofit that shrank them for one
that grows the box instead, leaving copy under 20 characters alone because the floor
does not apply to it; it returns how many boxes it touched. It cannot help
the other half of `type_floor` — copy stated at the floor and shrunk by its box — which
needs a bigger box rather than a bigger size (`ppt_layout.fits`, `text_size`).
**With a template bound, the title row is the template's.** `house_style` measures where
its own pages put one, so use the box it names — `title_row_box_corners_in`, or the
`title_row_as_code` line built from it
— and decide it **once, in the shared setup**, with every content page calling that one
thing. A page block that computes its own title coordinates is how a deck ends up with
three title rows (§8, §12).
Five of those raise a question a signature cannot answer:
- `replace_text(slide, "旧文字", "新文字")` finds whatever on the cloned page holds that
string and writes it — **this is how a template page is filled in**, one call per line the
page says. The key is the text the shape holds *now*, which is exactly what
`ppt_template(pages=[N])` prints above each shape, so a read-back is a list of keys to
paste. `replace_text(shape, "新文字")` writes one shape you already hold. The value may be
a list of `Run` instead of a string, so one word can carry the accent:
```python
s = clone_page(prs, prototype(tpl, 6))
replace_text(s, "单击此处添加文本",
[Run("访客中约 "), Run("84%", bold=True, colour=ACCENT_INK), Run(" 到访过夜市")])
```
Whatever a piece does not state is the template's — every piece is a copy of the run
the template put there, so the line keeps its face and size and one word of it does
not. This is the *only* way to emphasise a word on a cloned page: a plain string
puts the whole line into one run, and one run carries one colour. Pass a list of
those lists for several paragraphs.
- `replace_picture(shape, image, fit)` — `"contain"` shrinks the frame to the picture's
own proportions; `"cover"` crops the picture to fill the frame as it stands. Either way
it refuses a landscape figure in a portrait frame rather than squashing it.
The template's illustration is not always a picture: on a section page it is as often
a cartoon drawn as a group of freeforms, with no image to swap. Hand that group (or a
member of it, or a list of the loose shapes that make the drawing) to `replace_picture`
and a picture takes the drawing's box and its place in the z-order, the drawing gone --
a member stands for the wordless group around it, so the cartoon goes whole and the
card it sits in stays;
`replace_picture(shape_at(slide, 7), path, "cover")` does the same when shape 7 is drawn, and
`replace_picture([shape_at(slide, 5), shape_at(slide, 6)], path)` gives several loose
shapes way to one picture. An
illustration meant to sit on the template's ground is asked for with
`ppt_generate_image(..., transparent=true)`, which has it drawn on a green screen and keys the green out.
Three arguments decide *which* pixels a cover keeps, and none of them writes a file:
`anchor` is the side the crop keeps — `"centre"`, `"top"`, `"bottom"`, `"left"`,
`"right"` — so a photograph whose subject runs along the top survives at
`anchor="top"` where a centred crop cuts it; `trim=(left, right, top, bottom)` cuts
shares off the source's own edges *before* the fit, which is how a screenshot with a
progress bar along the bottom goes on the page (`trim=(0, 0, 0, 0.08)`); and `zoom`
is a multiple of the scale that just covers the frame, so `zoom=1.6` shows 1/1.6 of
it — a detail made legible at the size the frame has. Under 1 is refused, because a
cover that does not cover is `fit="contain"`. `trim` works on contain too, where the
frame gives way to what is left of the source. Do not pre-crop the file with PIL: a
crop written into a new file is a second copy to keep track of, it goes past the
proportion check here, and the template's own stale crop stays on the shape.
- `units(container)` returns runs of repeating sibling groups — a card row, an agenda list.
- `arrangement(run)` returns `("row"|"column"|"grid"|"irregular", rows, cols)`. Nothing
**moves** the survivors for you: closing the hole four units leave on a 2x4 grid is a
design decision, and `place` is how you make it. The same holds for growing: `add_unit`
extends a row, a column or a grid at its own pitch and refuses an irregular run -- two
cards set diagonally, pills on a curve -- because where the next one goes is yours to
say. Say it with `clone_shape(run[-1], (left, top, width, height))` and put the copy
where the design wants it (the refusal prints where the existing units sit); then
the copy takes `replace_text` like any other slot.
- `remove_unit(unit)` takes one slot out of a run and closes the gap, group and all. That
is how a page with more slots than the deck has points is cut down to size, and it is
the only operation `replace_text` cannot express on its own.
**Re-flow off the geometry the template already fixed.** `boxes(run)` hands back the
pitch and the size the template drew, and that tuple is exactly what `place` takes;
computing a position without it is guessing at coordinates the page already holds.
`ppt_theme` gives `THEMES` and `rgb`. `ppt_icons` gives `add_icon(slide, name, left,
top, size, colour, width_pt=None)` -- the pen is a twelfth of the side, the icons' own
2-on-24 weight, unless `width_pt` fixes it -- `swap_icon(slide, shape, name, colour=None)`,
`find_icons(term)` and `ICON_NAMES`.
**Repeated units, in detail.** A content page is usually one small group repeated, and
each repeat is written like any other line: `replace_text(slide, "the words that unit
holds", "yours")`, once per line. `ppt_template(pages=[N])` prints those words for every
shape on the page, unit by unit, so the read-back is the list of keys.
**Count as you read the render: units top row first and left to right, and inside a unit
the same.** That is the reader's order, not the file's -- a card's 60pt number sits above
its heading whatever order the designer happened to save it in. It matters for reading the
page back and for `units(slide)`, which returns the runs in that order.
**A slot fewer, and a slot more.** `remove_unit(unit)` takes a slot out and closes the row
up -- three cards left on a four-card row are re-spaced across the row's original width, a
2x4 grid losing two closes to six. `drop_shape` removes and leaves the hole, which is what
you want for a single shape and never for a slot. `add_unit(run_or_slide)` goes the other
way, copying the run's last unit and laying the run out again -- pass `units(slide)[0]` or
the slide for its longest run. Both refuse a run that follows no grid, because where the
next pill on an S-curve goes is the design's to say; there, `clone_shape(run[-1], (left,
top, width, height))` puts the copy where you say and the refusal prints where the existing
units sit.
**A number is the template's, and it does not renumber itself.** Cut an eight-slot agenda
to six and the survivors still read 01 to 06 because they were the first six; cut from the
middle and you write the numbers yourself, one `replace_text` each.
```python
s = clone_page(prs, prototype(tpl, 2)) # an agenda page of eight slots
for old, new in zip(TEMPLATE_LINES, sections): # six of them take this deck's sections
replace_text(s, old, new)
for spare in max(units(s), key=len)[6:]: # the seventh and eighth go
remove_unit(spare)
```
**A page can have two runs.** `units(slide)` returns them longest first, and a template's
four-card page is often two runs of two (cards 01-02 and 03-04, grouped in pairs). Nothing
special is needed -- `replace_text` is keyed on words and does not care which run a shape
sits in -- but check `units(slide)` whenever the render shows more repeated cards than you
wrote, because that is the page telling you there are slots you have not reached.
**Some pages have no run at all that `units` can see.** Content cards drawn as loose
shapes are not detected as a repeating unit, so `units(slide)` may return only the icon
badges beside them. `replace_text` reaches the cards anyway,
because it asks the page what it says rather than how it repeats. This is the reason the
route is words-first (§12, and the note under `remove_unit`): on such a page only
`remove_unit` needs a run, and only if you are cutting slots.
## 5. Figures come from the sources
`ppt_ingest` extracts them and nothing generates them. Never generate a substitute
for a real product UI, logo, person, scientific result, published figure or
statistical claim.
It reads PDFs, text, HTML, CSV, images, and office documents -- `.docx`, `.xlsx`,
`.doc`, `.odt` -- by converting those to PDF first, which needs LibreOffice. Anything
it could not read comes back as a finding naming the file: a source that is not among
the deck's evidence is one you must not write pages as though you had read.
**Look before you place.** `ppt_figure_inspect` returns a figure as an image with the
label its own source gave it, the width past which the bitmap softens, and the `concerns`
the extraction recorded — measured sentences you can act on or overrule, not a verdict.
Unseen, you cannot tell a legible plot from a scanned blur, a figure from a logo that
survived extraction, or a composite you should be cropping.
**Cite the label the source printed**, and only when that figure is the one on the page.
A page captioned "Fig. 4" showing Figure 5 is refused (§12).
**Keep source notes short.** One compact line at the bottom, such as
`来源:EverOS 官网;Mem0 官方文档。` A source note is a pointer, not a paragraph: URLs,
methodology caveats and analytical conclusions go in the body or the speaker notes.
A table in the sources is evidence to read, not a figure to place: retype it (§6).
**A picture set flush against a panel fights the panel's outline.** Inset it —
`box.inset(...)` gives the picture the padding the panel's own copy has.
Never distort aspect ratio, crop away interpretive labels, include a neighbouring
caption by accident, duplicate a printed caption, or leave transparency composited
onto black. If evidence is unreadable at the size it has: crop to the panel you
cite, give it more of the page, or rebuild it from the exact values.
Fetched pictures are first-class, not a fallback. Reach for the two paths of §2 whenever
a page names something with a face -- a product, a company, a standard, a published chart
-- not only when the materials left a hole; their results carry pixel dimensions, so a mark
that would land soft can be rejected before it is placed. `ppt_fetch` carries the URL with
the picture so a page can credit it, and the listing's own caption or alt text passed as
`ppt_fetch(caption=...)` travels into the same catalogue field a paper's figure fills. Copy
the page's words, never your reading of the picture.
**Generate at the shape of the frame it goes in.** `aspect_ratio` is the picture's shape,
and `replace_picture(..., "cover")` crops whatever does not fit: a 16:9 picture in a
banner strip keeps its middle third, a 16:9 picture in a portrait column keeps a
sliver. Choose the ratio nearest the frame (3:2 or 4:3 for a photo frame, 9:16 or 3:4
for a column, 16:9 for a wide band) and say in the prompt where the subject sits, so
what the crop keeps is the subject.
Plan every picture the deck will generate and ask for them in one call --
`ppt_generate_image(prompts=[{prompt, filename}, ...])` makes them at the same time and
ingests them once. Generate with `ppt_generate_image` only after both paths find no suitable existing
visual; a generated image illustrates a concept and never replaces evidence. Ask at the
shape of the region it will sit in — `aspect_ratio` takes `16:9`, `4:3`, `3:2`, `1:1`,
`2:3`, `3:4`, `9:16` or `21:9` and defaults to `16:9`, so a portrait strip gets a landscape
image to crop unless you say otherwise; a model that draws only a few frames answers with the
nearest one, and the reply's `width` and `height` say what came back, so fit it to the box
rather than assuming the ratio. The picture is drawn by whatever image model the host
configured, not by a fixed one: say the manner in words the model can act on. `references`
hands it pictures to match or vary -- the template's own illustration for its manner, an
earlier generation for a consistent series, a user's photograph to restyle. Fetched *numbers* are another matter: those are a source
the user did not choose, so anything the deck states as fact still comes from the
materials.
A cover does not need a figure. Its job is the title, who wrote it and where, and a
paper's Figure 1 pressed into its corner is smaller than the page it will get later.
**The cover and the closing page keep the template's composition.** Their title block,
chips, marks and rules are the design; a photograph or illustration in its own frame
beside them is the placeholder. Put the page's picture into that slot -- `replace_picture(layout_pictures(slide)[0],
image, "cover")` when the slot is on the layout, `replace_picture(shape_at(slide, n), image)` when
it is on the page --
and never drop the layout's art to make room for a full-bleed photograph (`drop_shape` refuses a
layout shape). Where the layout's picture is the whole page -- border, ground and
illustration in one bitmap, as the bundled templates draw their covers -- it is the frame
itself: keep it, and the second reader is told so.
What goes into the slot is decided by what the template put there, and the template can be
anyone's -- a bundled one, or a deck the user uploaded. Look at the slot in the render before
you ask for a picture: a slot that holds a photograph takes a photograph; a slot that holds a
drawing takes a cut-out in that drawing's manner, `ppt_generate_image(..., transparent=true)`
with the manner described from what you see (flat vector, line art, 3D render, watercolour;
its palette; its outline weight), even for a real place, and with the template's own
illustration passed as `references` so the model matches it rather than your description of
it. Never name a template or a house style you have not seen on the page. A template whose
cover *is* a photograph takes `backdrop`.
**A backdrop is the one generated picture that never poses as evidence.** A cover, a
contents page, a section page or a closing page wants atmosphere more than a figure, and a
template's own photograph there is a placeholder. All four take one where an image key is
configured -- the contents page included, which is the one that gets forgotten and comes
out as a list on a white field. Generate one with `ppt_generate_image` at the page's
shape (`aspect_ratio="16:9"` for a full page) and lay it behind everything with
`backdrop(slide, FIGURES / "cover.png")`: the photograph at full strength under a plane of the
theme's ink, with the page's type set light -- the form a reference cover that works uses. A
photograph washed to 30% on a white page under black type reads as fog, and the build reports
it (`washed_backdrop`). Write the prompt as subject, scene,
composition, style, palette and what to leave out (`no text, no logos, no faces`).
**The style is the subject's first and the template's second.** A real place, a street, a
market, a crowd, a product, a building, a meal -- anything the audience could stand in
front of -- is a photograph: `photographic, natural light, 35mm, no illustration`, and the
template's palette only in the grade (`warm amber light`, `cool teal shadows`). An idea that
has no face -- a process, a principle, a mood for a section page -- takes the template's own
manner, and an illustrated template draws it as an illustration. A cartoon night market on
a proposal to a district government reads as a children's book; a photograph of one reads
as evidence, which is why the real thing is found first (§2) and generated only when it
cannot be. Within the deck, one manner per kind: every place photographic, every concept in
the template's hand -- a photograph on one case page and a flat drawing on the next read as
two decks. A faint picture behind a template's own ground is the
texture form, `backdrop(slide, image, alpha=0.1, scrim=None)`, and it stops being texture past
0.12. Look at the render (§10): the contrast reading is taken off the pixels, so type that does
not carry over the picture comes back as unreadable type. A body
page dense with cards does not want one. The same knob exists for a picture already on the
page: a photograph the template drew or `replace_picture` filled that a title has to read over wants
a plane of ink over it and light type, the way `backdrop` lays one -- not `wash(shape_at(slide, n),
0.3)`, which is fog; a picture at full strength again is `wash(shape, 1)`.
### Look at the page before you ask for its picture
A generation happens while the page is still a plan -- prepare, brief, template, pictures,
*then* the program -- so a picture asked for at that moment is asked for against nothing.
Draft the page first: write its block, `ppt_build(slides=[n])`, and read the render. What
you ask for is then what the page **lacks**, not what it already carries.
**Hand the model the material; do not describe it.** `references` takes up to six pictures
-- the logo file, the mark the brand actually uses, the template's own illustration, an
earlier generation the new one is a series with -- as figure ids, source file names or
absolute paths. A described logo comes back as a different logo.
**Say where the type goes, and ask for no type.** Name the region the title needs by side
and by share -- "the left 55% of the canvas stays almost pure dark, reserved for title text
overlay" -- because a subject centred in the frame leaves the title nowhere to sit. And
`no text, no letters, no numbers`: generated lettering is wrong in every language, and
every word on a page is set by the typography.
**Then look again.** The picture was for a page, so rebuild that page and read the render:
only the composed page counts, and whether the type carries over what you laid down is
read off those pixels and refuses (§12).
**Where the deployment has no image key, `ppt_generate_image` says so** -- `no image API key
is configured` -- and that is an answer to pass on, not a page to leave open. Carry it on
type, grid, rule and colour, and say which pages would have had a picture. A blank slot, a
flat colour block standing in for one, or a body page's photograph moved onto the cover are
three ways of not saying it.
**Tables are for figures, and a deck has few of them.** A table earns its grid when a reader
compares numbers down a column -- a benchmark, a price list, a scoring matrix. Four labelled
points, a list of measures with a sentence each, a plan with phases: those are cards, a
timeline or a list, and set as tables they turn a deck into a report. One 20-page deck that
set five of them read as five spreadsheets; hold a deck to about one table per section, and
ask of each whether the reader compares figures in it or only reads it.
## 6. Charts and tables, drawn
Charts are shapes — rectangles, hairlines, marks and labels — which is what keeps
them editable and on the deck's palette. `matplotlib` imports in the program (the
engine uses it to typeset formulas) and is not how a chart is drawn here: a plot pasted
as a picture is off the palette, cannot be edited in the deck, and its labels are pixels
the type measurements never see.
**`ppt_charts` draws twenty-three of them for you**, and the mapping from a value to a
length is the half you must not write by hand. Import what the page needs:
```python
from ppt_charts import column, horizontal_bar, waterfall
column(slide, frame.body, T, [("East", 185), ("South", 142)], accent="East", unit="M")
```
The signatures are in §4.
### Which form, and what to draw when none of them can be
[deck/build/references/charts.md](deck/build/references/charts.md) carries the rest: the
twenty-three signatures, a row per form saying what it *encodes* (reference, not a
constraint), and what to substitute where the shape wanted cannot be reached from
rectangles and straight lines at all — a pie, a gauge, a radar, a sankey, an area chart.
Open it before choosing a form.
**The twenty-three are shortcuts, not a ceiling.** Their base is public on `ppt_charts`
too — the ink (`rect`, `disc`, `ring`, `hline`, `vline`, `poly`, `write_label`), the
scale (`span`, `snap`, `linear`), the palette discipline (`series_paints`,
`stack_paints`, `shades`, `emphasis`, `ink_on`, `contrast`) and the label fitting
(`type_face`, `text_width`, `pick_size`, `line_height`). Where none of the forms is the
shape of what the page argues, write the form; the rules below still hold. A chart you
write that raises `TooSmall` and returns a `Drawn` also works with `whether_a_chart_fits`,
`what_a_chart_will_do` and `the_smallest_box_a_chart_needs`.
### The rules a drawn chart obeys
- **Length is the value.** Every bar, segment and track is exactly proportional, and a
length axis starts at zero. A misdrawn axis is caught only by you looking at the render
(§10), which is the reason to let `ppt_charts` map the values.
- **Default — label the mark**, and leave the legend to the plot where no label can reach
its own. Name the units, and put the axis maximum where the reader can see it.
- **Default — one thing is accented.** The bar, segment or point carrying the claim takes
the accent and the rest take `muted` or `grid`; how many colours a chart ends up with
follows what it encodes, not a quota. `SERIES` in order is for a genuine multi-series
plot, and its later colours are quiet on a light ground, so give them the supporting
series.
- **Baselines and axes are hairlines.** The charts draw their own (§3 for what `rule` is
and is not). No vertical gridlines, no frame around a bar chart, no ground behind a plot
unless it separates layers.
- **Never invent a value.** No placeholder, no number reconstructed off a plot you cannot
read, no interpolated point. Where the series has a hole, the page says so.
For the compact bars beside a table or under a claim, a row of rectangles beats any chart
object: you control the length, the colour of the one bar that matters, and where its
value sits — `horizontal_bar` in a `body.rows(3)[2]` is that page. Do not reach for
`add_chart`: it arrives with Office's own six colours and gridlines, as recognisable a
tell as any stock template.
**Never draw a table with a bare `add_table`**, which arrives with Office's own look — a
white hairline around every cell, banding on, and a header style that fights the deck's
palette. A screenshot of a source table is no better: another typeface, and it cannot be
reweighted around the page's conclusion.
Everything above that ban is yours. **`ppt_layout.table()` is the shortcut** for the
ordinary comparison table — the Office look taken off, plus the part that is arithmetic
rather than design: **it sizes each column from what that column holds**, measures each
row from the lines its cells really wrap onto, and spreads the rows into the box you gave
it. Its look is defaults, and every one is a keyword away (§4). **Or draw the table
yourself** out of `Box`, `text_size`, `write` and `plane`, the right call whenever the page
wants a grid `table()` does not draw: merged cells, a header spanning three columns, an
icon inside a cell, a sparkline down one. Both paths, a worked hand-drawn table and the
seven `mark` kinds are in
[deck/build/references/tables.md](deck/build/references/tables.md). Open it when a page carries a table.
Compose freely around one: a two-product comparison may be two parallel regions with the
same row labels rather than one table, and a conclusion may sit on its own baseline
below.
Preserve units, scales, qualifiers, series meaning and source labels exactly.
## 6.5 The page's frame, and the helpers that fill it
```python
from ppt_layout import PAD, card, card_body_box, card_size, fits, heading, page, plane, stack
frame = page()
heading(slide, frame, T, "把任务定义抽象成查询,网络本身就与任务无关",
"02 统一范式 · 任务切换 = 换一组输入查询")
said = [("route", "分类头没了", "类别成了网络的动态输入,语义表示只通过损失监督学到。"),
("layers", "权重只有一套", "推理时按需拼装查询集合就能热切换任务,不重训。"),
("gauge", "代价", "四类任务合并统计 46.3 mAP,比四套权重各自训练高 0.2。")]
lanes = frame.body.inset(PAD, 0.0).columns(3)
tall = max(card_size(lanes[0].w, icon=i, title=h, body=b, font=FACE).h for i, h, b in said)
cell = stack(lanes[0]).take(tall)
if not all(fits(b, card_body_box(cell, icon=i, title=h)) for i, h, b in said):
tall = max(card_size(lanes[0].w, icon=i, title=h, body=b).h for i, h, b in said)
frame = frame.holding(tall + 2 * PAD) # the band is measured, so the body is cut to it
ground = frame.body
plane(slide, ground, T, tint="surface")
for box, (icon, head, body) in zip(ground.inset(PAD).columns(3), said):
card(slide, box, T, tint="background", icon=icon, title=head, body=body,
font=FACE, cjk_font=HAN)
```
**The body is the room a page may use, not the run it uses.** `page()` hands back all of
it, this page uses one measured band, and everything left over stays at the foot unless
the page says so: `frame.holding(tall + 2 * PAD)` cuts the body to the run and splits the
leftover as air above and below it. Ask it once the bands are measured and before a cursor
runs down them -- the kicker, the title and the footer do not move, so it reads the same
before or after `heading`, and a body the run already fills comes back unchanged. Pass the
bands and the gaps between them in the order they occur, and pass the taller run where two
lanes differ. **Not** for a run something else already spreads into the whole body:
`card_group(..., down=True)` given the body puts the leftover between its own cards, and a
body cut to the sum of their heights first leaves them touching. More on where the page's
slack goes below.
**`heading()` is one ready-made top edge** — a quiet ground behind the title row with the
kicker and the title set on it, stopping short of the body, and `bleed=False` to keep that
ground inside the safe area. It sets the title in `page()`'s own box rather than the
template's, so reach for it where you are composing a page yourself (§8) and where that box
agrees with the `title_row_box_corners_in` the template was measured for (§4).
**The header is one compact group.** A section tag, title and one explanatory line sit
close enough to scan as a single unit; the larger vertical break belongs between that unit
and the body. Tighten those internal gaps when the actual title runs to fewer lines than
the template's example did.
**Two claims in one frame read as one paragraph with a line break in it**, and putting a
mark in front of each does not change that reading. **A mark is not a structure.** A
single paragraph of explanation is a paragraph and nothing here is about one; what the
build reports as `listed_claims` is a box carrying two or more parallel claims (§12).
**What a card holds, and what it costs, before it is drawn.** `card` paints a rounded
plane in the tint you name and sets the icon's square, the title bold beside it and the
copy under it, all inside `PAD` (§7). It fills whatever box you hand it, so four cards off
`frame.body.columns(4)` are each as tall as the body.
`card_size(width, icon=, title=, body=, font=FACE).h` is how tall one has to be for what
goes in it, and `max(...)` across a set levels a row without padding each card out to its
cell; `fits(body, card_body_box(box, icon=..., title=...))` says whether the copy went in.
Both come off one estimate, which reserves for the widest face it knows when told none --
so where the unnamed call refuses the levelled height, take the height it asks for.
**Dividing a region.** `columns`, `rows` and `box.grid(cols, rows)` cut a box into cells,
row-major; a `stack` given `gutter=(box.h - n * tall) / (n - 1)` spends the region's slack
as air between its bands instead of leaving it in a heap at the bottom. `plane(slide, box,
T, tint="surface")` paints a region, and it counts towards `evidence` like any other
filled shape (§12).
`points(slide, box, theme, items, numbered=False)` writes a marked list, a hanging mark per
item and `numbered=True` for an ordered one; `points_size` measures one first, which
`text_size` cannot.
**Never guess a y coordinate.** `stack(box)` is a cursor down a region: `take(h)` hands
back the next band and moves on, `rest()` hands back everything still unspoken for, and
`skip(GUTTER)` puts the deck's one gutter between them. Bands are adjacent otherwise, so
heights from `table_size` and `the_smallest_box_a_chart_needs` add up to what the region
has. A coordinate you were *given* is different and you should paste it: `house_style`'s
`body_area_as_code` (§8) is measured off the template.
**Full inside, air between.** A component is measured and given exactly that much
(`card_size`, `text_size`, `picture_size`, `table_size`); the room the page has left over
goes *between* components, never inside one. A cursor runs from the top of its region, so
left alone it does the opposite -- every unused inch piles up underneath and the page has
a band of white along its bottom edge. Three calls spend it, all of them before the first
`take`:
- `stack(box).spread(*heights)` -- the leftover becomes the gaps, and the run ends on the
region's own bottom edge. A lane of cards, a table over a chart, and **two columns given
the same region, which is what makes them end level.** The gap it leaves is never under
`GUTTER`: components any closer read as one unfinished shape, so bands with no leftover to
share have to be shortened by the gaps between them rather than run flush against each
other. `card_group(..., down=True)` is this call with the cards' heights measured for you.
- `stack(box).centre(*heights)` -- half above and half below, for a run that nearly fills
its region and sits against something taller beside it.
- `page().holding(*heights)` -- the page's own leftover, taken out of the body before a
cursor runs down it at all: the body is cut to the run these heights add up to, and what
is left becomes air above and below that run instead of a band along the foot. Not half
and half -- the white over the body is the `GUTTER` under the heading and the white under
it is the `MARGIN`, so an even split still leaves 0.44in more at the foot; the two are
measured from the heading's edge and to the page's and come out reading equal. **This is
the call when the page's bands were measured and taken off a cursor** -- a table over a
card row, a figure over its conclusion -- because that is where the leftover has nowhere
else to go: spread into the one gap between two groups instead, a whole page's slack is
reported as a blank field in the middle rather than one at the foot. It moves the region
and not a cursor, so `columns`, `split_left` and a stack down each lane all come out of
the same corrected band and two lanes still end level; where the lanes differ, pass the
taller run. **Not** for a run something else already spreads into the whole body:
`card_group(..., down=True)` given the body spends the leftover between its own cards, and
a body cut to the sum of their heights first leaves them touching. A body the run already
fills comes back unchanged, and so does one the run overruns -- growing it would put type
through the safe margin, and an overrun is `take`'s to refuse with both numbers.
**Spread components, never running copy.** The gap between two cards is air; the gap
between two paragraphs is a break in a thought, and three sentences pulled 1.9in apart
stop reading as one column at all -- rendered and compared, it is worse than leaving them
at the top. So when a copy column comes out far shorter than its region, neither call is
the answer: that is a page bigger than what it holds, and it is answered by turning the
copy into components (the same three sentences as three cards fill the column and land
level with the figure beside them) or by the page having more to say.
**And a chart is as tall as the box you hand it.** A bar's length is the reading, so a
chart given a short box draws short bars and leaves the page empty under them -- the same
page then looks crowded and unfinished at once. Give it the band (`down.rest()`, or the
height `short_by` says is free), not a box guessed at the size the chart "should" be.
**The shape most pages want, in four lines.** An unequal split, the narrow side laid
out to the body's own floor, one of its bands divided again, and a group on every
surface. It answers all four of the things a reader complains about at once -- both
columns end level, the columns come off one grid, every group has an edge, and the page
ends where it means to:
```python
frame = page(footer=True)
write(slide, frame.title, title, size=TITLE_PT, colour=T["foreground"], bold=True, font=F, cjk_font=HAN)
left, right = frame.body.split_left(0.545) # the picture side and the reading side
picture_fit(slide, img, left, T, font=F, cjk_font=HAN) # no caption strip: it goes in the foot
PLAN = (1.05, 1.05, 0.86, 0.86) # measured, then checked:
assert stack(right).short_by(*PLAN, *((GUTTER,) * (len(PLAN) - 1))) == 0.0
down = stack(right).spread(*PLAN) # the leftover becomes the gaps
claim, pair, rows, note = (down.take(h) for h in PLAN)
for box, tint in ((claim, "foreground"), (rows, "surface"), (note, "accent_soft")):
plane(slide, box, T, tint=tint) # every group on a surface of its own
for cell in pair.columns(2, gutter=0.15): # one band divided again, same grid
plane(slide, cell, T)
footer(slide, frame.footer, T, note=f"{source};{cap}", font=F, cjk_font=HAN)
```
`short_by` counts the bands and not the gaps between them, so pass the n-1 gaps in with
them -- `spread` never puts less than `GUTTER` between two components, and a plan that
fits without them still raises at the band where the room runs out.
**Measure every band, ask `short_by`, then draw.** `take` refuses a band the region
cannot hold, and it refuses at the band that asked -- which is the last one, not the one
that was too tall. So a plan checked band by band as it is drawn fails at the bottom of
the page with nothing said about the top, and the fix is never where the refusal is.
Measure all of them first, hand the heights to `stack(box).short_by(*heights)`, and it
answers how many inches the whole plan runs over, or 0.0. Take that off the bands that
can give it up -- the ones whose content is not the page's claim -- before the first
`take`. This is the most common way a build script dies.
**Give a content page its foot -- on the pages you compose.** With a template bound the
foot follows the template: where its own pages carry none, add none, and a photograph's
source lives in the figure catalogue, not in a credit line squeezed onto the page.
`page(footer=True)` then `footer(slide, frame.footer,
T, note=...)` on every composed page that is not the cover, a divider or the closing. Two things
come of it and neither is the number: a body that ends two thirds down an otherwise
blank page reads as unfinished, and a rule across the foot is what says the page ends
there because it was meant to. The number is a `slidenum` field, so inserting a page
ahead of it does not make it wrong.
```python
frame = page(footer=True)
...
footer(slide, frame.footer, T, note="来源:公开报道整理", font=F, cjk_font=HAN)
```
**A cloned page can have a foot, but look at the strip first.** Cloning a template
example leaves you no `Frame`, and the strip does not need one: it is a fixed box on the
canvas, and `page().footer` and `page(footer=True).footer` are the same
`Box(0.72, 6.48, 12.61, 6.78)`. What it does need is to be empty. Measured across the
bundled templates, most of their example pages use that band themselves -- 18 of 25 pages
of one and 14 of 18 of another have a shape whose ink reaches into it, and one page's
reaches 7.68in, past the canvas. Drawing a foot there puts the note over the template's
own design; rendered, the source line came out across a pill.
So measure the page you cloned before writing into its foot, and where the band is taken,
the template's furniture is the page's furniture -- leave it, and put the source in the
one page of the deck that does have room, or on the page you composed yourself.
```python
slide = clone_page(prs, prototype(tpl, 7)) # the template's page; replace_text per line
strip = page().footer
taken = shapes_in(slide, strip, share=0) # anything of the template's reaching in
if not taken: # the band is the template's, or it is yours
footer(slide, strip, T, note=source, font=F, cjk_font=HAN)
```
A template that paints its own band across the foot needs `colour=` for a tone that
reads on that band — the theme's `muted` is chosen against the page's ground, not
against a band the master drew.
**Then ask what the page gave up.** The other way a plan goes wrong is quieter: the
measuring calls answer rather than refuse, so a ramp walk that ran all the way down to
`BODY_FLOOR_PT` and a formula that had to break at a separator both hand back a value and
the page looks measured. `what_this_page_gave_up()` is the ledger of those, cleared each
time it answers -- call it once the page is drawn and read it before the build does, or
the first you hear of the concession is a `type_floor` on a page you thought you had
fitted.
```python
plane(slide, band, T)
points(slide, band, T, rows, size=BODY_PT, font=F)
for gave in what_this_page_gave_up():
print(f" gave up {gave['what']}: asked {gave['asked']}, got {gave['got']}")
```
```python
rows = [["任务", "四套权重", "一套权重"],
["实例分割 VIS", "46.1", "46.3"],
["语义分割 VSS", "52.4", "52.6"],
["全景分割 VPS", "50.8", "50.9"],
["指代分割 RVOS", "61.2", "61.4"],
["四类合并", "46.1", "46.3"]]
answer = "四类任务一套权重,切任务只换输入查询。"
fig, cap = f"{FIGURES}/fig2.png", "Figure 2:架构(论文原图)"
grid = table_size(rows, T, size=BODY_PT)
said = text_size(answer, frame.body.w - 2 * (PAD + 0.10), size=LEAD_PT, bold=True, font=FACE)
share = 1 - grid.w / (frame.body.w - GUTTER)
above, _ = frame.body.split_top(1 - (said.h + 2 * PAD) / (frame.body.h - GUTTER))
figure, _ = above.split_left(share)
down = stack(frame.body)
tall = max(grid.h, picture_size(fig, figure).h)
figure, lane = down.take(tall).split_left(share)
picture_fit(slide, fig, figure, T)
table(slide, lane, rows, T, size=BODY_PT)
down.skip(GUTTER)
band = down.take(said.h + 2 * PAD)
plane(slide, band, T, tint="accent_soft")
write(slide, band.inset(PAD + 0.10), answer, size=LEAD_PT, bold=True, colour=INK,
font=FACE, cjk_font=HAN, anchor="middle")
```
**`take(said.h + 2 * PAD)` and not `rest()`, and that is the whole of what keeps this page
from being a void.** `rest()` hands back everything still unspoken for, so a tinted plane
drawn on it is as tall as whatever the bands above happened to leave -- and this one holds
one line. `said.h` is already in hand two lines up, which is the point of measuring: the
band is cut to the sentence, and room left over stays unpainted rather than being covered.
Copied without that, this fragment is a colour panel several times the height of the copy
inside it. Reach
for `rest()` when what goes in the band is sized to the band by construction -- a
`picture_fit` filling it, a table with `weights` -- and never for a plane with a sentence
on it.
**Cut the region to what goes in it, in both directions -- and cut the band to the figure,
not the figure to the band.** `table_size(rows, T)` with no box answers both directions,
and the lane and the band above are cut to its two fields (§4), the band taking the larger
of its `.h` and what `picture_size` says the figure needs. `picture_fit` scales an image to
fit its box whole without cropping and centres what is left, so a band chosen by eye leaves
a strip of white above the figure and another below; `picture_size(fig, region)` reads
the image's own pixels, and handing that `.h` to `stack.take` leaves the centring nothing
to centre in. A lane chosen by eye fails the other way: what sits in
it prints past its own edge, which the file measures as fitting and only the render shows.
## 7. Icons, on the cards and on the blocks beside them
```python
from ppt_layout import card
from ppt_icons import add_icon, find_icons, ICON_NAMES # 1304 Tabler Outline names
card(slide, box, T, icon="target", title="语义查询是必要的",
body="去掉 Qsem 改用线性分类头:YouTube-VIS 44.7 对 46.3")
add_icon(slide, "clock", Inches(0.7), Inches(2.1), Inches(0.42), ACCENT)
```
**On a cloned page the icon is the template's, and `swap_icon` changes it.** A template's
icon is a freeform path holding no text, so `replace_text` passes it by and a page about supply
chains keeps the trophy the template drew. `swap_icon(slide, shape, name)` draws `name`
where that shape was, as large as it was and in its colour -- a theme slot stays a theme
slot, so the icon keeps following the deck's palette -- and removes the old one. `shape`
is what `shape_near(slide, x, y)` or `shape_at(slide, n)` found, the `# [n]` numbering
of the read-back page:
```python
from ppt_icons import swap_icon
slide = clone_page(prs, prototype(tpl, 12)) # then replace_text per line
swap_icon(slide, shape_near(slide, 6.2, 2.0), "truck") # the second badge's glyph
```
It refuses a shape bigger than an icon: a photograph wants `replace_picture`, a panel
`drop_shape`.
**`card(icon=)` takes one, `card_group` carries it into a whole row or column of them, and
so does a block drawn without a card.** Not every block needs one. Pick the icon for what the block argues, not for a noun in its title. An icon
takes the card's first line whether or not a title shares it, so what room the copy has
left is `card_body_box`'s answer (§6.5).
`add_icon` is for the icons that are not on a card: beside a kicker, in the corner of a
metric, at the head of each band of a hand-drawn panel (§6.5). Stroked vectors, so they
scale and stay editable after export, in whatever colour you pass. The ink is inset in the
square by no fixed fraction -- from nothing of its height (`minus`, a rule) to nearly all
of its width (`json`) -- so a row of icons centred on their squares is not centred on its
ink. `the_ink_an_icon_covers(name, size)` is the box the strokes really cover, and
`add_icon` hands back that box measured off what landed.
**Check the name before you write it, with a shell rather than a build.** Every name is
a key in `deck/build/icons.json`, so `grep '"wave"' deck/build/icons.json` answers in a
shell and costs nothing:
```bash
python3 -c "import json;print([n for n in json.load(open('deck/build/icons.json')) if 'wave' in n])"
```
**A guessed name is not free.** `add_icon` raises `LookupError` on a name that is not
there, and a raise ends the whole script -- every page after it goes unwritten and the
build round is spent. The refusal
does name the nearest three, which is why guessing feels cheap; it is cheap only if the
guess was the last thing the script did.
**Search by meaning, not by filename.** `find_icons("deadline")` returns `calendar_due`
and `find_icons("risk")` returns `warning`: every icon carries its upstream tags and
shelf and the search reads them, so the word you would use on the slide is a good enough
query. It is a function in the build script, so it answers on the next build -- put
every icon you are unsure of in one `print(find_icons(...))` and read them all from one
round. Ask one concrete word at a time: `inventory` reaches `building_warehouse` through
its tags, while an abstraction of several words -- `supply chain`, `throughput` -- matches
on neither name nor tag and comes back with nothing usable or nothing at all. A few hundred
names need no lookup at all, grouped by what they are for in
[deck/build/references/icons.md](deck/build/references/icons.md).
Keep them small -- an icon is a mark beside type, so the square follows the line it labels
rather than a size of its own -- and give each real space. On a card that arithmetic is
`card()`'s and not yours.
## 7.5 Processes, flows and timelines
A sequence is drawn with the shape that means sequence: five rectangles with gaps between
them is a list, and a spine with five stops on it is a process. `timeline`, `connect` and
`preset` are how, and never by computing the geometry yourself.
**There is no `chevron_row`.** Its band of interlocking arrows arrived on every deck, and
it could not line up with anything under it -- interlocking steps advance by
`(width - notch)` where `box.columns(n)` advances by `width + gutter`, so copy beneath the
row sat on a second grid. `preset(slide, box, T, "chevron")` still draws a single arrow.
[deck/build/references/shapes.md](deck/build/references/shapes.md) has the signatures, the
label box to write into, and the knobs that are angles in degrees rather than fractions.
Open it when the page has a sequence, a branch or a route.
## 8. Inside a user's template
Its master, theme, layouts and canvas are the deck's house style, and a deck whose theme
is not the template's is refused (§12).
**A template has structural pages and editable content examples.** For a content page,
start from the nearest example by information shape: replace its text and pictures, delete
spare repeated units, and move or resize the surviving regions when the content needs it
-- a prototype is a starting composition, not an immutable form. Where no example carries
the page's information shape, compose the page instead (below) -- for at most a quarter of
the content pages: the outline refuses a plan that composes more than that, because a
deck that redraws its pages inside a template has stopped being the template's however
carefully it measures the house style. Measured: 3 of 17 composed read as the template's
own; 6 of 17 read as "drawn by hand and ugly", and the six were exactly the composed ones;
17 of 17 read as "nothing but self-drawn layouts".
**Even a page you compose starts on the template's page.** Clone the nearest example,
`remove_unit` the units the page does not need, and draw into the region that frees -- the
header row, the panels, the marks and the ground stay the template's, and only the body
is yours. A page drawn from primitives alone carries none of them, and reads as drawn by hand.
`ppt_template(pages=[n])` gives the example's real geometry to draw against.
**`ppt_template(project=..., pages=[4, 5])` reads an example page back as the python-pptx
that would draw it** — up to six pages a call, and as many of them as fit one reply; the ones
left out are named in `pages_not_read`, so ask for those in a second call. It is flat and literal: every position in
inches, inherited sizes and colours resolved, groups opened, and a `# [n]` above each
shape, which is the numbering `shape_at` uses. The
page's own pictures are written into the build directory beside your script, so an
`add_picture("template_00.png", ...)` line in it runs as pasted, and the imports it needs
head the block as comments. Read a page this way when you need its real numbers -- a card
row's pitch, the title's exact box, the accent it really uses. What it cannot reproduce is
emitted as a comment naming itself -- a custom-drawn shape, a gradient, a pattern fill or
a semi-transparent fill -- and those are the pages to clone rather than redraw.
**A chart on a template page is the exception to that clone.** The layout may be cloned,
but the chart must be redrawn with `ppt_charts` from your own data and placed in the same
position; the read-back page and the line beside the render both name the box it occupies.
Cloning it keeps the template's own numbers and nothing here rewrites them: a chart's
labels live in its own part rather than in a text frame, so `replace_text` does not reach
them, and no gate reads inside a chart either -- a page shipped this way shows the
template's own categories and nothing refuses it. Drop it off the clone --
`drop_shape(shape_near(slide, left, top))` -- and draw yours into that box.
### The pages you clone
`ppt_template` names them —
`house_pages: {"cover": 1, "agenda": 2, "section": 3, "closing": 11}` — and renders them.
Those four are what a reader recognises the house by: a deck that draws its own cover
announces itself as not the user's before a word of it is read.
```python
from ppt_template import clone_page, drop_shape, prototype, remove_unit, replace_picture, replace_text, shape_at, units
tpl = Presentation(os.environ["PPT_TEMPLATE_SOURCE"]) # the original, with its example pages
s = clone_page(prs, prototype(tpl, 1)) # prototype(tpl, N) counts from 1, like the menu
replace_text(s, "科幻未来·推动行业革新", "项目标题") # the key is the text that shape holds NOW --
replace_text(s, "探索通用行业创新创业新机遇", "本次汇报副标题") # ppt_template(pages=[1]) prints it
replace_picture(shape_at(s, 7), FIGURES / "selected-figure.png", "cover")
drop_shape(shape_at(s, 3)) # a line this cover does not use
# "20XX.XX.XX" left alone stays as written
s = clone_page(prs, prototype(tpl, 2)) # the contents page: one unit repeated
replace_text(s, "目录", "目录")
for old, new in (("第一部分标题", "市场现状"), ("第二部分标题", "需求侧"), ("第三部分标题", "落地路径")):
replace_text(s, old, new) # one call per line the page says
for spare in max(units(s), key=len)[3:]: # the slots this deck has no sections for
remove_unit(spare) # removed and the row closed up
```
**Every line is named by the words it is holding.** There is no by-role shortcut and none
is needed: `ppt_template(pages=[N])` reads the page back with the text of every shape
printed above it, and each of those strings is a key `replace_text` takes. Read the page,
then write one call per line. Writing "目录" and "Agenda" back as two new boxes over the
clone instead is the one construction `template_underlay` refuses.
**A string that matches nothing raises, and lists what the page does hold.** That listing is
the fix: paste the string it prints. Matching is on the words in the box and never on a
shape's name, so `drop_shape(shape_saying(s, "Presenter name"))` removes the cover line
that says that, while a real shape name out of the file matches nothing.
**Drawing into a cloned page means clearing the space first.** `replace_text` swaps
words where they stand, and that is all it does; the moment a chart, a panel or a figure
of your own goes onto a cloned page, whatever the template drew in that space is still
under it. `clear_region(slide, box)` empties the box -- every shape drawn in it, groups
walked into -- where `box` is a `Box.corners(...)`, a `Box.at(x, y, w=, h=)` or a
`page_box(shape)`, because the two corners a `Box` holds and the `(left, top, width,
height)` a template reference prints are the same four numbers and a bare tuple of them
is refused rather than guessed at -- and returns what it removed, with `left_standing` for anything that still
lies over the box. Read that. The shape the box sits *inside* is kept on purpose: on a
template that is the card the chart was drawn in, and the card is the arrangement the
page was cloned for. `keep=("the words a shape shows",)` or `keep=(7,)` spares one, and
`share=` (half by default) decides how much of a shape has to be inside the box to go.
Do not write the sweep yourself: a sweep keyed on a shape's language or size keeps the
arrows, the number labels and every connector (a rule is 0.00in tall), and the chart goes
on top of them. Only a region takes everything in it.
```python
slide = clone_page(prs, prototype(tpl, 4))
replace_text(slide, "the template's title", "System prices fell 31% in one year")
print(clear_region(slide, Box.corners(0.95, 1.95, 12.55, 6.55))) # say what went
column(slide, Box.corners(0.95, 2.30, 4.15, 6.05), T, prices, unit="$")
```
A shape's own box is a region, so clearing exactly where the template drew something is
`clear_region(slide, page_box(shape_at(slide, 3)))` -- `page_box` answers in the two
corners a region is given in, with the scale the shape's group applies to it.
**Numbering: an integer is the shape's place on the page**, as the read-back prints it
(`# [n]`, groups opened, counting shapes that cannot be drawn as well as those that can).
`shape_at(slide, n)` counts the slide as it stands **now**, so it is the same numbering only
while nothing has been removed -- `drop_shape` and `remove_unit` both renumber everything
after them. Prefer `replace_text` keyed on words, which no deletion disturbs; where you do
need an index, take the handle before you delete anything.
**A cloned page keeps the template's words, so a line you did not replace is still saying
them.** That is this route's safety rather than a nuisance: the miss is visible on the page,
and the build refuses to publish it and quotes the line back (`placeholder_copy`, §12).
There is no third answer -- a block is replaced, or taken off the page with `drop_shape`.
The one line that stays is a structural page's own label: the `目录` or `Agenda` a template
writes on its index page is what that page is called, and the check does not ask for it back.
The alternative was measured and rejected: a call that emptied what it was not told about
turned the same mistake into a page holding a title over twenty blank boxes with every
check green, and a blank page is a page nothing can name.
**An emptied shape is not a removed one.** Writing `""` empties the text and leaves the
box standing, because the box is often the design -- a tinted panel, a numbered circle. So
`""` is not a spelling of delete: written into agenda slots a deck has no sections for, it
ships the numbered bubbles anyway. `drop_shape(shape)` takes a shape off the page --
reach it with `shape_saying(slide, "the words it holds")` or `shape_at(slide, n)`;
`remove_unit(unit)` takes a whole slot out and closes the row up. Reach for a shape by the
words it holds or by the box you kept, not by counting to 7 again after something was
removed.
**Never lay a new text box over a page you cloned.** The construction is `clone_page` for
the background, `add_textbox` for the copy, `replace_text` never called — and it ships the
template's own "单击此处添加长一点的副标题" under your own text on every page.
`placeholder_copy` and `template_underlay` both refuse it: if you cloned a page, every word
on it arrives by replacing a word that was there.
**The template's photographs are placeholders.** A cover's stock photograph of a meeting
table says nothing about the deck's subject: replace it with a figure from the sources or
take the frame out — `replace_picture(shape_at(s, 7), f"{FIGURES}/fig3.png")`, or
`drop_shape(shape_at(s, 9))`. An image under a
tenth of the page is different: an icon, a corner flourish or a rule, and part of the
design.
**A landscape figure does not go in a portrait frame.** `replace_picture` places it anyway
and **warns** when the two are more than 2x apart, saying both numbers and what the fit
did -- contained it sits as a strip in an empty frame, cropped it loses about that share
of the figure -- and the warning comes back in the build reply's `warnings`. A photograph
cropped to a banner is a crop a designer makes; a chart that lost its outer columns is
not, so look at the render and, where the figure matters, give the frame the box it needs:
`replace_picture(shape_at(slide, 4), f"{FIGURES}/fig2.png", box=(0.8, 1.6, 7.4, 4.2))`, where the
four numbers are `(left, top, width, height)` in inches — a **size**, the same one `place` takes, and not
the two corners a `ppt_layout.Box` holds. A `Box` handed over whole is converted; what you
must not do is unpack one into four numbers, because `Box.corners(0.72, 1.24, 12.6, 6.7)`
read as a size draws a 12.6x6.7in frame off the side of a 13.33in page.
**Some of a template's photographs are on its layouts, not its pages.** Several bundled
templates carry the cover's, the section page's and the closing page's picture on the
layout, so every page built on it inherits the same picture and a `replace_picture` on the
cloned page never reaches it -- `template_picture` cannot see it either; `layout_picture`
names the layout, and `ppt_template` lists them as `layout_pictures`. `layout_pictures(slide)`
returns those shapes, largest first, and `replace_picture(layout_pictures(slide)[0],
FIGURES / "cover.png", "cover")` changes the picture for every page on that
layout at once, which is what a house photograph should do. A picture the size of the page
is the page's background: the template's own is a soft texture the type reads over, and a
photograph swapped in at full strength drowns every title on that layout, so it takes the
`alpha`. A picture generated in the deck's own style (§5) is the usual replacement; keep the
template's own when it is the design -- an illustration the designer drew, not a stock
photograph.
### The pages you borrow
**When no example of the bound template carries the page's information shape, borrow one
from another bundled template before composing.** `ppt_template` lists them under
`borrowable_pages`, each by template, page and arrangement -- an S-curve of five pills, a
ring of six labels around a hub, a photograph beside a numbered list. A borrowed page lands
on this deck's own layout of the same name and its theme colours resolve to this deck's,
so what comes across is the arrangement and nothing of the source's look.
```python
from ppt_template import bundled, clone_page, prototype, replace_text
slide = clone_page(prs, prototype(bundled("gold_panel_year_end_summary"), 13))
for old, new in zip(read_back_keys, ["选址", "招商", "运营", "推广", "评估"]):
replace_text(slide, old, new)
```
Record both on that page of the plan -- `borrowed: "gold_panel_year_end_summary"` and
`prototype: 13` -- so the checks that read the plan open the right file: which page the
clone promised, whose placeholder copy and photographs to look for. `bundled()` takes the
file name without `.pptx` and names the ones that ship when it is wrong. Only the content
pages ppt_template listed are offered: the deck's cover, index and closing are always the
bound template's own.
Everything above about a cloned page holds for a borrowed one -- `replace_text` per line,
`remove_unit` for the slots the content does not fill, whatever you replace in neither
still says the source template's example copy, and its photographs are placeholders.
**Colours follow the deck; two things do not, and ppt_template names both.** Every fill,
line and run that refers to a theme colour resolves to this deck's theme the moment the
page is cloned -- do not walk the page's shapes to recolour them, there is nothing to
find, and on python-pptx merely reading `shape.line.color` puts an outline on every shape
it touches. What does not follow: a picture is a bitmap, so an illustration painted in
the source template's accents arrives in those accents -- `replace_picture` it out; and
a label whose colour read on the source's card can land on a tint of this deck's that
cannot show it -- ppt_template lists those pairs, and the build refuses the page under
2:1 as `unreadable`, naming the label. Answer that by writing the label in this deck's
ink or filling its card darker, one line each, and leave the rest of the page alone.
### The pages you compose
**Adapt first, and compose what no example can carry.** A page started from the nearest
example by information shape arrives already in the house style -- the pitch of its rows,
its title's own box, its accents, the spacing its designer chose -- and none of that is
work you then have to do. Filling one leaves nothing behind either: `remove_unit` and
`fill` delete the units your content does not fill, so a six-card prototype carrying four
cards comes out as four cards. Compose where no example carries the page's information
shape, and say which example you looked at in `needs`; a deck none of whose pages came
from the template's own comes back as `template_adherence` (§12).
**Start the page from the layout the template's own content pages sit on.** That is
`layout_for_a_page_you_draw` in the measured `house_style`, and `add_slide` on it brings
the template's background with it — the corner device, the edge rules, the ground colour,
none of which you have to draw, which is what makes a deck of composed pages read as one
deck. `add_slide` on a blank layout, or painting your own background, is how a deck ends up
with the template surviving as a colour.
The rest of `house_style` is the frame for what you then put on it:
```
layout_for_a_page_you_draw: "Title Only" # add_slide on this, and the background comes with it
title_row: at (0.72, 0.14) 11.88x0.98in, 28pt, Arial, left-aligned, anchored bottom, on 7 of its pages
title_row_as_code: from ppt_layout import Box, write; TITLE_ROW = Box.corners(0.72, 0.14, 12.6, 1.12);
write(slide, TITLE_ROW, claim, size=28, bold=True, align="left", anchor="bottom",
colour=INK, font=FACE, cjk_font=HAN)
type_pt: {title: 28, subtitle: 24, body: 18, secondary: 16, caption: 14}
face: "Arial"
safe_area_corners_in: [0.72, 0.14, 12.6, 6.7] # two corners, what Box takes -- not a size
body_area_corners_in: [0.72, 1.24, 12.6, 6.7]
body_area_as_code: from ppt_layout import Box; body = Box.corners(0.72, 1.24, 12.6, 6.7)
```
```python
from ppt_layout import Box, write
T = THEMES[next(iter(THEMES))] # the template's own palette
layout = next(l for l in prs.slide_layouts if l.name == "Title Only")
slide = prs.slides.add_slide(layout) # the background comes with it
write(slide, Box.corners(0.72, 0.14, 12.60, 1.12), "消融:时序颈与语义查询是关键",
size=28, bold=True, align="left", anchor="bottom", # title_row_as_code, pasted
colour=T["foreground"], font=FACE, cjk_font=HAN)
body = Box.corners(0.72, 1.24, 12.60, 6.70) # body_area_as_code, pasted
```
and what goes in `body` is a structure out of
[deck/build/references/layouts.md](deck/build/references/layouts.md) with its modifier
layers, chosen for what this page argues (§3.5).
**Match the title row's anchor, not just its box.** The box is where the row is and the
anchor is where the ink lands in it, and the two readings of one 0.98in row are most of an
inch apart: a master that says `anchor="b"` sinks a single-line title to the bottom of the
row, while `write` anchors to the top unless told otherwise. `title_row_as_code` is that
call with the anchor already in it, and a composed page whose anchor disagrees with the
template's comes back as `title_row` (§12).
The subtitle row is reported the same way — and a template may set its own **centred**
under a left-aligned title, which is its designer's decision and not a mistake to
correct. Take the alignment from `subtitle_row_as_code` rather than assuming it matches
the title's.
**One scale for the whole deck.** Every page title at `type_pt["title"]`, body copy at
`type_pt["body"]`, captions at `type_pt["caption"]`, the same role at the same size on
every page (§3), and the floors of §3 under all three. `type_drift` (§12) measures it off
the render.
**Set every size yourself, and give the box the room it needs.** `write` turns autofit off
on purpose, so a box that cannot hold its copy comes back as a measurement rather than as
type quietly dropping to 10.8pt. The answer to `overset_copy` is a taller box, a wider
column, less copy or a second page — never a smaller size (§12).
**Stay inside the safe area.** `safe_area_corners_in` is where the template keeps its own content;
a layout's artwork lives outside it, and copy laid across that artwork comes back as
`over_layout_art`.
**Ink is `foreground`. `surface` and `background` are what the ground is painted with.** On
a dark template those three are white, near-black and black, so reaching for "the dark
one" as a type colour puts near-black type on black. One line is measured off the render,
against the ground each box actually landed on: under **2:1** nothing is legible and
`unreadable` refuses the deck (§12). Nothing is said above that line, so everything
between "legible" and "comfortable" is yours to judge off the render (§10).
**Take the face from the page, not out of the theme.** `ppt_theme`'s `font_family` is
what the template's *theme* declares, and a template can declare 微软雅黑 while every run on
its example pages is Arial. `house_style`'s `face` is what the pages actually use — pass
it as `font=` and its CJK companion as `cjk_font=`, or a line of mixed text comes out in
two unrelated faces.
Which pages are cloned is decided in the outline (§2), not here. A page there that names
no prototype is listed back to you as a question — pick the nearest content example, or say
in `needs` why no example carries this page's information shape. A page that named one and
was not built on it comes back after the build as `prototype_kept`, which reports rather
than refuses (§12).
## 9. Formulas
**Never set an expression with `write`, and never fake a fraction or a root.** A
fraction, a root, a sum with limits is TeX -- `formula(slide, box, r"\frac{QK^{T}}{\sqrt{d_k}}", T)`
sets it as a typeset picture in the deck's ink; a slash and a `√` character read as
code. A formula in a text box is prose: it wraps where
the box runs out, and where it runs out is the middle of a symbol — and every subscript in
it is flat, so the one thing the notation carried is gone (`flat_formula`, §12).
`formula()` sets it as one unbreakable line, steps down the ramp until it fits, and splits
at the expression's own separators at the floor; `formula_type_size` says which step it
will land on. [deck/build/references/formulas.md](deck/build/references/formulas.md) has
the notation it reads. Open it when a page carries mathematics.
## 10. Look at the deck
`ppt_build` renders a **batch of pages** back per call, each labelled with its number
and anything measured on it, and says how many of the deck's pages it has not shown you.
A build without `slides` shows the pages whose code changed since you last saw them --
the ones you just wrote or edited -- several at a time, and walks the deck from
`page_from=` once every page has been shown; `slides=[4, 5, 6]` asks for exactly those
pages instead. So looking at the deck is a few calls rather than one per page, and after
an edit pass the plain build is the call to make: it comes back with what you changed.
How many a call carries is configurable, and the tool schema names the numbers. **A
program that ran without error is not visual evidence.** Open each render and name
something concrete on it before you change it.
**Reference — angles to look from, not a checklist and not a score.** What the code
measures is in §12; these are the readings nothing measures:
- a role at the wrong step of the scale — a caption as large as the body, a title a
point above its subhead, a source line as large as what it credits;
- unreadable, distorted, blank or badly cropped figures;
- empty charts, weak labels, bars out of proportion to their values, misleading scales;
- icons too small to read, or crowding the text they belong to;
- a sparse or mechanically forced layout — compartments where an argument belongs;
- anything on §3's avoid list.
Two questions settle most of it: is the smallest type readable with the page shrunk to a
third -- roughly the back of a lecture theatre -- and, beside the page before it, is this a
different page or the same page with different words.
The reply names how many pages it did not show and how to ask for them: **a named subset
is not the deck** (§11).
**A page that raises no longer stops the deck.** The build runs your `# SLIDE n` blocks
one at a time: a block that raises loses only its own page -- a page saying what went
wrong stands in its place -- and every other page is still drawn, measured and shown.
The failure comes back as a `page_failed` refusal with the traceback, beside the
findings on the pages that did draw, so fix it in the same edit pass as those rather
than building once per crash. Keep the shape that makes this work: shared helpers in
the prelude above the first block, one slide per block, nothing a later block needs
defined inside an earlier one.
**Fix everything a build reported before you build again, and send the edits and the
build in one reply.** A build's reply lists every finding on every page it showed; answer
all of them -- one `edit_file` per page named -- and then `ppt_build`, all in the same
reply: tool calls run in the order listed, so the edits and the build cost one turn where a
reply per call costs two. One build per finding is the shape of a slow run.
**Name the pages you changed when a page is wrong.** `ppt_build(slides=[7])` rebuilds and
hands back page 7 alone: edit that page's block, look at it, fix it, look again. A sweep tells you twelve pages have something wrong; a short loop tells you what.
Run the full build when a batch is settled; that is what runs the gates and publishes.
**What has to clear is the refusals. Everything else you judge once.** A refused build is
not delivered, so §12's refusals get answered whichever way you read them. The warnings
are reports and the renders are the judge: read each one on the page it names, fix what
you agree with -- and what looks wrong to you whether or not anything measured it -- and
leave a finding you have looked at and disagree with alone. Judge it once: the same
warning on the next build is the same warning, and rebuilding a deck nothing refuses to
chase it changes nothing.
**Somebody else looks, and you do not have to ask: `ppt_review`.** A build with nothing
refusing it runs this by itself once five of its pages have not been read -- a draft
counts, so the first reading arrives while the deck is still being written -- and the list
comes back in that same reply under `first_reading`, with the pages it found the most on
rendered beside their entries. It reads the pages nobody has read yet, and a page you
rewrite after it was read is read again, so the build that delivers the deck reads
whatever is outstanding. Call it yourself for a second reading once you have answered the first, or
`ppt_review(pages=[7, 8])` to read back the pages you have just changed. It renders every
page and reads each one on an empty context -- one page, its planned claim, and the
requirements -- and hands back a problem list: where a region carries nothing, where copy crowds a rim, what does not line up,
which group of cards has no icon in it, a figure too small or squeezed or off-centre in
its own region, a column much wider than anything in it, type a step off its role, and
whether the page still carries the claim it was planned for. `pages=[7, 8]` reads just
those; omit it for the deck. It reports which of the two kinds a blank region is, so the
entry names the answer as well as the defect.
It refuses nothing, publishes nothing and edits no file. Every entry is yours to judge on
the same terms as a warning: look at the page it names, fix what you agree with, leave
what you have looked at and disagree with -- and say why.
Saying why is a call, not a sentence in your reply. Every entry goes into a ledger with an
id (`p14-a1b2c3`), and every build reply lists what is still open under `open_findings`
until it is answered one of two ways: a re-reading of that page no longer sees it, or you
look at the page and dismiss it -- `ppt_review(project, dismiss=[{"id": "p14-a1b2c3",
"reason": "the illustration is the template's design"}])`, which reads nothing and returns
the list as it stands. An entry reported again on a later reading keeps its id and shows
how many times it has been seen. A deck delivered with open entries is a deck whose reader
was never answered, and the reply says so on every build.
One reason is refused: "it is the template's own illustration". The template's stock
pictures and drawings are placeholders, and keeping the template's style means its
layouts and colours, not a whiteboard meeting on a page about elderly care. A `figure`
entry about one of them is answered by replacing the picture -- `ppt_generate_image` in
the template's own style, or a real photograph from `ppt_image_search` -- or by saying
what the picture depicts and why that is this page. A dismissal that only says whose
picture it is comes back as `refused`, and the entry stays open.
**One page at a time, and a verdict on a kind is not a judgement.** The entries already
carry their kind; what makes the list worth having is that each one names a page. "Most of
these are whitespace on the table pages, and the cover art is the template's, so keeping
them" answers eighteen pages without opening one. If you are going to leave an entry, open its page, say what you saw
there, and name the page when you say it. Dismissing a kind is how a list of eighteen
becomes a list of two without anything being read, and the ones a category sweeps away are
the ones the reviewer could see and you could not.
**Why a second reader rather than more looking.** You wrote these pages, and a region you
filled on purpose reads to you as a decision already made -- so a warning against it is
one you have already answered. The reviewer has none of that: it did not choose the
layout, and it is not told your program, only the plan -- so it sees the colour panel
four times the height of its two lines, and the table that stops well short of the band
its card row fills, which cleared every gate.
**When the pages read right, stop.** A build nothing refuses has delivered the deck, and
after `ppt_review` there is no further pass, no stage that rearranges the pages once the
gates clear. So a page you send on unlooked-at ships as it is, and a deck you have read
through -- and had read back to you -- and would not change is finished.
**Where the user named the file's destination, the build delivers there.** State it once as
`ppt_build(deliver_to="/absolute/dir/name.pptx")` -- or a directory ending in `/` to keep
the deck's own name -- on any build, a draft included; it is kept for the deck, and every
build that publishes writes `out/` and copies the same bytes there, the first time and on
every revision after. The reply comes back with `delivered_to` and the slide count, and
those are the words to give the user. One file lands: the `.pptx`. The render under
`out/` is the engine's own preview and is not delivered or handed over. A copy you make
with `exec` is recorded nowhere, is not the deliverable, and is called out as such.
## 11. The order is enforced, not suggested
`ppt_build` refuses until `ppt_prepare` has read the task, and until `ppt_brief` holds the
three things the user decides. And it refuses a deck carrying a page whose code has never
been rendered back to you: the record is keyed on the block that drew each page, so a page
you looked at stays looked at and a page you then edited does not. A draft render counts --
it is a page put in front of you -- so a deck drafted through page by page has nothing left
for the publishing build to refuse. `slides=[1]` on an eighteen-page deck is not a way to
ship seventeen pages nobody saw.
That one cannot be answered by editing the deck. Run the build again, walk the rest of the
deck a batch at a time with `page_from`, and look.
One more thing the order decides: **`deck/ingest/materials.md` does not exist until
`ppt_ingest` has run.** Reaching for it first gets a `No such file or directory`. When it
is there, `ppt_ingest` returns `materials_index` — every part of it with the line it starts
at — and `read_file` takes `offset` and `limit`. Read the parts the deck stands on rather
than the file: whatever you read is re-sent on every request after it, so a single
whole-file read is the most expensive thing in the deck.
## 12. What the build refuses, and what it only reports
**Refused**, with the page named. Five land at the outline, before a line of the program
is written:
| Refused at the outline | The move |
| --- | --- |
| a **figure id the catalogue does not hold** | take the id from the catalogue |
| a **layout id the catalogue does not carry** -- `P1` to `P41` are page structures, `M1` to `M26` modifier layers, not zero-padded | open [deck/build/references/layouts.md](deck/build/references/layouts.md); an id not in it can only come from not having opened it |
| with a template bound, an outline whose **cover, index and closing** do not name the template's own pages | name them |
| when ingest extracted no figures at all, a **cited page nobody opened** | `web_fetch` each URL the materials cite and take its image links, then `ppt_fetch` what you will use -- or the PDF behind an abstract, so `ppt_ingest` extracts its figures |
| with a template bound, a plan **composing more than a quarter of its content pages** from scratch -- neither a `prototype` nor a `borrowed` page | give the rest the nearest example by information shape and adapt it; keep composing only the pages whose shape no example carries, and say which in `needs` |
| a **`borrowed` naming no bundled template, or a page of one that is not a content page** | take the template and page from `borrowable_pages` in the `ppt_template` reply; the bound template's own pages take `prototype` alone |
| a plan starting **more of its content pages on one prototype** than on all the others together -- four pages at least, and only where this template's own unused examples can carry the repeats | the reply names an example per repeating page by what that page says it carries; record `prototype: N` on those pages. Keep the one prototype for pages that are a series a reader compares across, and say in `needs` which shape no other example carries |
For each URL that holds nothing usable, or will not load, say so in `ppt_outline`'s
`swept`:
`[{"url": "...", "found": "text only, no figures"}]` -- kept with the deck, so a later
call does not ask again. Nothing is recorded until all four clear.
The rest land on the built deck:
- a page **citing one figure while showing another**;
- a **length the brief did not agree**, or **the wrong language**;
- a page the build **cannot map back to** a block of your code;
- a page whose **block raised** while the build ran it on its own (§4): a page saying so
stands in its place and the rest of the deck was still drawn, so fix that block in the
same pass as the other findings;
- a page you have **never been shown**;
- a page whose plan **promised a figure and that shows no picture** -- place it, or
plan the page again without it;
- a page whose plan **planned a table and that shows none** -- draw it with
`ppt_layout.table()`, or plan the page again without one;
- type a reader **cannot make out**: under 2:1 against the ground it landed on;
- words **colliding in the render**, which is not answerable by shrinking them;
- copy **set above where its box starts**: a frame anchored to the middle or the bottom
of its box holds its last line on the box's bottom edge, so a line the box cannot hold
is added *above* the first one -- off the top of the page, or over what the page
already drew there. Anchor the frame to the top, or `place()` it: the box, the copy and
every neighbour stay as they are. Nothing here asks you to cut a line, and a frame that
shrinks its own type to fit is not in this at all;
- the template's own **placeholder text** still on a page you cloned, or that page
cloned for its background with **new text boxes laid over** it;
- a page you **cloned and never wrote into**: under the copy its kind of page needs,
and holding more empty text frames than frames with words in them. A frame written
with `""` is emptied, not removed, and a page of them is a page that says nothing --
write every line of the page with `replace_text`, and `drop_shape` or `remove_unit`
what the page does not use.
**Reported**, with a page number, and every one of them yours to judge:
| Reported | What it means | The move |
| --- | --- | --- |
| `band` | a **filled colour bar** carrying nothing: a full-width band -- 85% of the page or more, under 1.3in tall -- with no copy on it, or a narrow accent strip -- 0.22in or less on its short side, six times that on its long -- wherever it sits and whatever sits on it | put the copy on the band, or take the strip out. Under 4.5pt it is a hairline and nothing here looks at it. Bars encoding values are left alone, but only where the row reads as data off its geometry alone: two or more slots, and lengths that differ |
| `type_floor` | **type under the floors** of §3 | a taller box, a wider column, less copy or a second page -- never a smaller size |
| `type_scale` | body copy over the 14pt floor but under `BODY_PT`, at a size **not a step of the ramp** | `size=BODY_PT`, `size=LABEL_PT`, or `the_largest_step_this_copy_takes(text, box, font=F)` |
| `type_drift` | **one slot the deck sets at several sizes** | give the boxes the height one size needs |
| `row_type_drift` | **one row whose cards came out at different sizes**: the copies of one slot inside a single repeating unit, measured against each other rather than against the size the slot was drawn at. Every box is behaving -- each shrank its own copy to fit -- and the row is what reads wrong, so no per-box reading can see it | even out how much the units hold, or set that slot one size explicitly. Giving one card more height fixes that card and leaves the row uneven |
| `outranked_title` | a page whose **title and the line under it came out at one size**, with the line under it the longer of the two, so a reader meets the caption as the heading. On a cloned page the title states no size at all -- it resolves through the layout and the master -- so the number your caption matched is one nothing in your program shows you; the finding names it | state the caption a step down (`size=BODY_PT`, or `size=LABEL_PT`), or shorten it until it reads as a caption. Where the caption box is the template's own, its size came with the page rather than from your program: state a smaller one there and leave the title alone |
| `evidence` | a deck with **too few content pages showing anything** -- a figure, a table, a chart or a diagram, counted across the deck rather than page by page | `plane` is a filled shape like any other, so three cards come to three and the same three on a plane come to four |
| `wide_table` | a **table too wide to read** | fewer columns, or the page turned over to it |
| `native_table` | a table still **wearing Office's own look** | `ppt_layout.table()`, which takes the banding and gallery style off and keeps every row -- or draw the grid yourself, which never had them |
| `rule_strike` | a **rule struck** through a row | `rule` sits below the box it underlines; place it off the row |
| `card_overflow` | copy **escaping a card** | `fits(body, card_body_box(box, icon=, title=))` first, then the height it asks for |
| `crowded_panel` | a line **crowding its panel**'s bottom rim | the padding your other panels have, or one line less |
| `covered_shape` | content three fifths **hidden behind an opaque shape** drawn after it, read off the file's z-order rather than off the render | draw the covering shape first, or shrink it off what it hides. It reads the file and not the page, so a shape that covers nothing a reader was looking at is yours to dismiss -- the render-side check that can prove a reader lost something is `word_collision`, and that one refuses |
| `off_page` | a shape **over the page edge** | inside `safe_area_corners_in` |
| `house_style` | a theme that is **not the bound template's**, read off the theme's colour scheme slot by slot | open the deck with `Presentation(os.environ['PPT_TEMPLATE'])` rather than `Presentation()`. If you repainted the theme deliberately, this is the finding to leave alone -- `ppt_template(repaint=true)` is the way to do it that keeps the two in step |
| `literal_escape` | copy **printing an escape** as characters (`48.3\nOVIS`, ` `, `_x000B_`) | pass a real line break -- a newline in the string, which becomes a new paragraph -- or split the copy into two values. A paragraph carrying a quote or a bracket is read as showing code and is left alone |
| `boxless_copy` | copy in a **box with no height**, or no width, so the page shows nothing of it however much it says | give the box the size its copy needs (`text_size`, `card_size`), or take the block off the page. Read off the declared geometry, so a box your own code sized to zero is the case it was written for |
| `spilled_copy` | copy **painted off the page** by a `wrap=False` box | turn wrapping on and give the box a second line's height |
| `over_layout_art` | copy **on the layout's artwork** | inside `safe_area_corners_in` |
| `flat_formula` | an **expression set as prose**, so its subscripts are flat and the box may break it inside a symbol | `formula()` (§9) |
| `listed_claims` | one box holding two or more parallel claims, so they **read as a list to be read out** | separate them so a reader can see where one ends; §6.5 has the calls that measure a region before it is drawn |
| `orphan_line` | a **label the render broke** onto a second line, as "为什么要统 / 一", in a text box or a table cell | a hair more width, or fewer characters |
| `wrapped_label` | a label wrapped **in a box too narrow for it** | the width `text_size` says the line takes |
| `overset_copy` | **copy that does not fit** the box it was put in | the taller box it names; `text_size`, `card_size` and `points_size` say the same number before the build does |
| `clipped_copy` | copy the file states that the render **clips instead of wrapping** | turn wrapping on |
| `excessive_whitespace` | a **large blank field** between body groups, below the content, or inside a panel | grow the load-bearing content, redistribute it, or shorten the panel |
| `unseparated_blocks` | two groups with **no more air between them** than inside them | a wider gap, a surface, or a hairline |
| `undivided_body` | a body of **three or more blocks of copy with nothing drawn between them** -- no panel, no rule, no outline, no figure. Its sibling above is about the air between two groups; this one is about whether the division was drawn at all | one device is the whole floor: `card_group` for blocks of one kind, `plane` behind the region that answers the page, `rule` between two halves that are not cards, or a figure the copy is set against (§3.5) |
| `title_row` | titles that start at **different left edges**, or sit at a different anchor inside that row than the template's own | `title_row_as_code`, decided once in the shared setup (§8) |
| `layout_variety` | composed pages that **nearly all resolve to one page structure**, read off the shapes each page actually carries | a structure chosen for what each page argues (§3.5) |
| `page_mapping` | a build whose **pages cannot be told apart** in what the run recorded -- a separate reading from the refusal above, which is about the `# SLIDE` blocks in your file where this one is about what the run made of them. The two can arrive together | one block per page, each composing its own page |
| `template_adherence` | a deck **none of whose pages came from** the template's own structural pages | clone the cover, the contents list, the divider and the closing (§8) |
| `template_picture` | the template's **photographs still showing** | `replace_picture(shape_at(s, n), ...)` a figure in, or `drop_shape` |
| `placeholder_marks` | the template's **numerals and marks left on a page** -- its glyphs, its lone tokens -- said once per page with a few named, where a phrase of the template's refuses on its own | keep the ones the design draws, `replace_text` or `drop_shape` the rest |
| `prototype_kept` | a page built on a **prototype other than the one its outline named** | either page reads, so build it or rewrite that outline line |
At the plan, four more: a page planned with one line and nothing else, a deck spending a
quarter of itself on pages carrying no argument, material too thin for the pages agreed,
and a page drawn from scratch where the plan named no prototype -- that last one arrives as
a question, to answer with an example page or a reason in `needs`. At ingest, a source that
could not be read at all.
**Warnings are mostly not refusals for a reason.** Nearly every one of them can be
"fixed" by making the type smaller, and a gate that demanded they clear would get exactly
that. The two that do refuse -- words over words, and content behind a panel -- are the
two that shrinking makes worse.
**Three silences the build reports rather than hides.** With nothing ingested there is no
figure catalogue, so a page crediting Figure 4 while showing Figure 5 would not have been
caught. With no brief recorded, neither the deck's length nor its language has been
checked against anything agreed. With no renderer on the machine, nothing measured on the
rendered page ran at all. None of the three stops a deck, all three mean "not checked"
rather than "clean", and all three belong in what you tell the user.
**What nothing checks at all:** whether a number or a name on a page is one the sources
printed. There is no index of what the materials state and no gate that reads one, so
every figure on every page is yours to have read correctly and yours to be right about.