slotdeck 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -6
- package/dist/deck/alternatives.d.ts +34 -0
- package/dist/deck/alternatives.js +370 -0
- package/dist/deck/alternatives.js.map +1 -0
- package/dist/deck/design.d.ts +40 -0
- package/dist/deck/design.js +112 -0
- package/dist/deck/design.js.map +1 -0
- package/dist/deck/validate.d.ts +18 -0
- package/dist/deck/validate.js +38 -2
- package/dist/deck/validate.js.map +1 -1
- package/dist/index.d.ts +22 -1
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +7 -0
- package/dist/node.js +29 -0
- package/dist/node.js.map +1 -1
- package/dist/plan/catalog.js +3 -1
- package/dist/plan/catalog.js.map +1 -1
- package/dist/plan/edit.js +6 -2
- package/dist/plan/edit.js.map +1 -1
- package/dist/plan/plan.d.ts +10 -0
- package/dist/plan/plan.js +8 -0
- package/dist/plan/plan.js.map +1 -1
- package/dist/plan/review.d.ts +43 -0
- package/dist/plan/review.js +141 -0
- package/dist/plan/review.js.map +1 -0
- package/dist/plan/write.d.ts +5 -0
- package/dist/plan/write.js +11 -4
- package/dist/plan/write.js.map +1 -1
- package/dist/preview/pagus.d.ts +30 -0
- package/dist/preview/pagus.js +45 -0
- package/dist/preview/pagus.js.map +1 -0
- package/dist/preview/patterns.d.ts +27 -0
- package/dist/preview/patterns.js +35 -0
- package/dist/preview/patterns.js.map +1 -0
- package/dist/preview/provider.d.ts +30 -0
- package/dist/preview/provider.js +11 -0
- package/dist/preview/provider.js.map +1 -0
- package/dist/render/render.d.ts +18 -1
- package/dist/render/render.js +28 -8
- package/dist/render/render.js.map +1 -1
- package/dist/render/repeat.d.ts +17 -2
- package/dist/render/repeat.js +150 -4
- package/dist/render/repeat.js.map +1 -1
- package/dist/render/text.d.ts +6 -0
- package/dist/render/text.js +24 -0
- package/dist/render/text.js.map +1 -1
- package/dist/template/budgets.d.ts +33 -0
- package/dist/template/budgets.js +38 -0
- package/dist/template/budgets.js.map +1 -0
- package/dist/template/change.d.ts +18 -1
- package/dist/template/change.js +33 -3
- package/dist/template/change.js.map +1 -1
- package/dist/template/check.d.ts +3 -0
- package/dist/template/check.js +53 -3
- package/dist/template/check.js.map +1 -1
- package/dist/template/derive.d.ts +54 -0
- package/dist/template/derive.js +402 -0
- package/dist/template/derive.js.map +1 -0
- package/dist/template/draft.d.ts +7 -0
- package/dist/template/draft.js +17 -1
- package/dist/template/draft.js.map +1 -1
- package/dist/template/instruct.js +11 -5
- package/dist/template/instruct.js.map +1 -1
- package/dist/template/manifest.d.ts +22 -0
- package/dist/template/manifest.js +17 -1
- package/dist/template/manifest.js.map +1 -1
- package/dist/template/sample.d.ts +17 -0
- package/dist/template/sample.js +27 -2
- package/dist/template/sample.js.map +1 -1
- package/dist/text/fontkit.d.ts +18 -0
- package/dist/text/fontkit.js +91 -0
- package/dist/text/fontkit.js.map +1 -0
- package/dist/text/fonts.d.ts +45 -0
- package/dist/text/fonts.js +59 -0
- package/dist/text/fonts.js.map +1 -0
- package/dist/text/layout.d.ts +12 -0
- package/dist/text/layout.js +104 -0
- package/dist/text/layout.js.map +1 -0
- package/dist/text/measure.d.ts +85 -0
- package/dist/text/measure.js +369 -0
- package/dist/text/measure.js.map +1 -0
- package/dist/text/styles.d.ts +123 -0
- package/dist/text/styles.js +417 -0
- package/dist/text/styles.js.map +1 -0
- package/dist/tools.d.ts +12 -0
- package/dist/tools.js +32 -6
- package/dist/tools.js.map +1 -1
- package/docs/README.md +15 -5
- package/docs/api-reference.md +278 -8
- package/docs/changelog.md +135 -0
- package/docs/chat-tools.md +22 -5
- package/docs/deck-format.md +6 -0
- package/docs/design-checks.md +216 -0
- package/docs/generating.md +4 -1
- package/docs/layout-freedom.md +255 -3
- package/docs/manifest-format.md +19 -3
- package/docs/preview.md +217 -0
- package/docs/providers.md +1 -1
- package/docs/rendering.md +8 -0
- package/docs/template-studio.md +110 -11
- package/docs/text-fit.md +456 -0
- package/docs/troubleshooting.md +13 -2
- package/package.json +7 -1
package/docs/text-fit.md
ADDED
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
# Measuring text with fonts
|
|
2
|
+
|
|
3
|
+
Without fonts, slotdeck judges whether a text fits its slot by counting characters against a budget
|
|
4
|
+
(`maxChars`). A budget cannot see where lines break, and letters differ in width from one typeface and one
|
|
5
|
+
language to another, so it is a rough guard.
|
|
6
|
+
|
|
7
|
+
Given the template's font files, slotdeck measures a text the way PowerPoint sets it: it breaks the text into
|
|
8
|
+
lines at the slot's typeface and size and compares them with the room the slot's box gives. Validation then
|
|
9
|
+
says "takes 7 lines; the box holds 5", the planner repairs by that, and text can be set a little smaller to fit.
|
|
10
|
+
|
|
11
|
+
This is optional. A template loaded without fonts behaves exactly as before.
|
|
12
|
+
|
|
13
|
+
## Setup
|
|
14
|
+
|
|
15
|
+
Measuring needs a library that reads font files. slotdeck has an adapter for `fontkit` and does not depend on
|
|
16
|
+
it: install it yourself and pass the module in.
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
npm install fontkit
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { readFileSync } from 'node:fs';
|
|
24
|
+
import * as fontkit from 'fontkit';
|
|
25
|
+
import { fontkitMetrics, loadTemplate, renderDeck } from 'slotdeck';
|
|
26
|
+
|
|
27
|
+
const fonts = fontkitMetrics(fontkit, {
|
|
28
|
+
// typeface name as the template states it: one file, or several for its weights and slants
|
|
29
|
+
Calibri: [readFileSync('fonts/calibri.ttf'), readFileSync('fonts/calibrib.ttf')],
|
|
30
|
+
Cambria: [readFileSync('fonts/cambria.ttc'), readFileSync('fonts/cambriab.ttf')],
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const template = loadTemplate(readFileSync('templates/demo.pptx'), manifest, { fonts });
|
|
34
|
+
const { pptx, warnings } = renderDeck(template, deck, { assets });
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
That is all: everything that takes the template now measures text. Nothing else in your code changes.
|
|
38
|
+
|
|
39
|
+
`fontkit` ships no TypeScript declarations. In a TypeScript project add a file such as `fontkit.d.ts` with one
|
|
40
|
+
line: `declare module 'fontkit';`.
|
|
41
|
+
|
|
42
|
+
### Which fonts a template needs
|
|
43
|
+
|
|
44
|
+
`templateFonts(template)` lists the typefaces the template's text slots are set in, with the weights and
|
|
45
|
+
slants used:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { loadTemplate, templateFonts } from 'slotdeck';
|
|
49
|
+
|
|
50
|
+
templateFonts(loadTemplate(pptx, manifest));
|
|
51
|
+
// [ { typeface: 'Calibri', bold: false, italic: false },
|
|
52
|
+
// { typeface: 'Calibri', bold: true, italic: false },
|
|
53
|
+
// { typeface: 'Cambria', bold: true, italic: false }, ... ]
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Pass a language as the second argument (`templateFonts(template, 'he')`) when decks are written in a language
|
|
57
|
+
other than the template's: Hebrew or Arabic text is often set in another typeface than Latin text (see
|
|
58
|
+
"Which typeface a character is set in" below), and that typeface is then listed too.
|
|
59
|
+
|
|
60
|
+
On a machine that has the fonts installed, `fontFiles` from `slotdeck/node` finds the files in a folder by the
|
|
61
|
+
family name each file states:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import * as fontkit from 'fontkit';
|
|
65
|
+
import { fontkitMetrics, loadTemplate, templateFonts } from 'slotdeck';
|
|
66
|
+
import { fontFiles } from 'slotdeck/node';
|
|
67
|
+
|
|
68
|
+
const names = templateFonts(loadTemplate(pptx, manifest)).map((font) => font.typeface);
|
|
69
|
+
const files = fontFiles(fontkit, 'C:/Windows/Fonts', names); // or '/Library/Fonts', '/usr/share/fonts/truetype/...'
|
|
70
|
+
const template = loadTemplate(pptx, manifest, { fonts: fontkitMetrics(fontkit, files) });
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`fontFiles` reads one folder and does not look into subfolders. Scanning a system font folder takes about a
|
|
74
|
+
second, so do it once at startup.
|
|
75
|
+
|
|
76
|
+
### What `fontkitMetrics` accepts
|
|
77
|
+
|
|
78
|
+
- TrueType (`.ttf`), OpenType (`.otf`), collections (`.ttc`), WOFF and WOFF2, as `Uint8Array` or `Buffer`.
|
|
79
|
+
- A key is the typeface name **as the template states it**, matched without regard to case. The value is one
|
|
80
|
+
file or an array of files. Regular, bold, italic and bold italic are told apart by what each file says of
|
|
81
|
+
itself, so the order does not matter.
|
|
82
|
+
- From a collection, the font whose family name equals the key is taken; when none does, the first.
|
|
83
|
+
- A weight or slant that was not supplied is measured with the nearest face that was (a bold from a regular
|
|
84
|
+
file is made slightly wider, as a synthesized bold is). A variable font supplies its own bold. Results
|
|
85
|
+
measured this way are marked `approximate`.
|
|
86
|
+
- A character the typeface has no glyph for is measured in the first supplied typeface that has one, and the
|
|
87
|
+
result is marked `approximate`.
|
|
88
|
+
- A typeface that was **not supplied at all** is not guessed: slots set in it are not measured and keep their
|
|
89
|
+
character budget. `checkManifest` and `textMeasurer(template).missingFonts()` name such typefaces.
|
|
90
|
+
|
|
91
|
+
### Fonts you may not copy
|
|
92
|
+
|
|
93
|
+
Calibri, Cambria, Arial and most fonts that come with Windows or Office are licensed with that software.
|
|
94
|
+
Check your licence before copying their files to a server.
|
|
95
|
+
|
|
96
|
+
A file may stand in for a typeface of another name. Some open fonts were drawn to the exact widths of a
|
|
97
|
+
licensed one, so text measures the same:
|
|
98
|
+
|
|
99
|
+
| Template typeface | Open font with the same widths | Note |
|
|
100
|
+
| --- | --- | --- |
|
|
101
|
+
| Calibri | Carlito | Checked against PowerPoint for English text: the same line counts at 12 pt and up, nearly always below. Carlito has no Hebrew or Arabic letters. |
|
|
102
|
+
| Cambria | Caladea | Checked together with Carlito. Latin only. |
|
|
103
|
+
| Arial, Times New Roman, Courier New | Liberation Sans, Liberation Serif, Liberation Mono | Drawn for this purpose; not checked here. |
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
const fonts = fontkitMetrics(fontkit, {
|
|
107
|
+
Calibri: [readFileSync('fonts/Carlito-Regular.ttf'), readFileSync('fonts/Carlito-Bold.ttf')],
|
|
108
|
+
Cambria: [readFileSync('fonts/Caladea-Regular.ttf'), readFileSync('fonts/Caladea-Bold.ttf')],
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Any other substitute gives lines that are close but not exact. For a Hebrew or Arabic template, supply the
|
|
113
|
+
template's own font files: line counts depend on the real letter widths.
|
|
114
|
+
|
|
115
|
+
## What changes once the template has fonts
|
|
116
|
+
|
|
117
|
+
| Where | Without fonts | With fonts |
|
|
118
|
+
| --- | --- | --- |
|
|
119
|
+
| `validateDeck` (pass `measure`), `renderDeck` warnings | `text is 410 characters; the slot fits about 352` | `text takes 7 lines at 18 pt; the box holds 5 (2 lines too many). Text of about 290 characters fits.` |
|
|
120
|
+
| Text longer than its budget that does fit | warning | no warning: the measurement decides |
|
|
121
|
+
| Text within its budget that does not fit | no warning | warning |
|
|
122
|
+
| `planDeck`, `writeDeck`, `generateDeck`, `planEdit`, `editDeck`, the chat tools | a draft over a character budget goes back to the model | a draft that runs over its box goes back to the model, with the lines it takes and the length that fits |
|
|
123
|
+
| `renderDeck` with `shrinkText` in the manifest | nothing | text that does not fit is set smaller, within the limit you state |
|
|
124
|
+
| `checkManifest` | checks that slots point at real shapes | also tries every budget out and reports one that lets text run over |
|
|
125
|
+
| `sampleDeck(template, { fit: 'box' })` | same as the default | fills every text slot to what its box really holds |
|
|
126
|
+
|
|
127
|
+
What does **not** change: the budgets the planning model is told (`maxChars`), the paragraph limit
|
|
128
|
+
(`maxParagraphs`, still reported as before), and tables and charts, which are not measured. To give the model
|
|
129
|
+
budgets that match the boxes, see "Budgets from measurement" below.
|
|
130
|
+
|
|
131
|
+
`validateDeck` takes a manifest, not a template, so it gets the measurer as an option:
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import { textMeasurer, validateDeck } from 'slotdeck';
|
|
135
|
+
|
|
136
|
+
const issues = validateDeck(template.manifest, deck, { layout: 'adaptive', measure: textMeasurer(template) });
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`renderDeck`, the planner and the studio functions take the template and need nothing extra.
|
|
140
|
+
|
|
141
|
+
### The warnings
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
text takes 7 lines at 18 pt; the box holds 5 (2 lines too many). Text of about 290 characters fits.
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
- `7 lines` is what the text takes at the size it will be rendered at, over all its paragraphs.
|
|
148
|
+
- `the box holds 5` is how many lines of this text the slot's box has room for.
|
|
149
|
+
- `Text of about 290 characters fits` is the length to cut to. It is worked out from this text, so it is in
|
|
150
|
+
the text's own language and letter widths. The planner sends this sentence to the model as it is.
|
|
151
|
+
- `about 7 lines` (with "about") means a typeface or weight was stood in for, so the count is close, not exact.
|
|
152
|
+
- `even at 15.5 pt` means the text was already set as small as `shrinkText` allows and still does not fit.
|
|
153
|
+
|
|
154
|
+
For a box that does not wrap its text, the warning reads
|
|
155
|
+
`text is 30% wider than its box at 12 pt, and the box does not wrap. Text of about 40 characters fits.`
|
|
156
|
+
|
|
157
|
+
## Shrinking text to fit
|
|
158
|
+
|
|
159
|
+
Set `shrinkText` on the manifest to let text that does not fit be set smaller:
|
|
160
|
+
|
|
161
|
+
```json
|
|
162
|
+
{
|
|
163
|
+
"version": 1,
|
|
164
|
+
"name": "demo",
|
|
165
|
+
"direction": "ltr",
|
|
166
|
+
"shrinkText": { "to": 0.85, "minSize": 10 },
|
|
167
|
+
"patterns": []
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
| Property | Type | Meaning |
|
|
172
|
+
| --- | --- | --- |
|
|
173
|
+
| `to` | number | The smallest share of its size a text may be set at. `0.85` lets 20 pt text go down to 17 pt. |
|
|
174
|
+
| `minSize` | number, optional | A size in points under which no text is set, whatever `to` allows. Use it to keep small body text readable. |
|
|
175
|
+
|
|
176
|
+
How it works:
|
|
177
|
+
|
|
178
|
+
- Only text that does not fit is touched, and only as far as needed: the size goes down in half points until
|
|
179
|
+
the text fits, so one long title becomes 32 pt and the others stay 34 pt. A shrunk text must fit with a
|
|
180
|
+
quarter of a point of width to spare, so that it does not sit exactly on the edge.
|
|
181
|
+
- The code writes the new size into the slide. It does not rely on PowerPoint's own "shrink text on overflow",
|
|
182
|
+
so the file looks the same in every viewer.
|
|
183
|
+
- A text that fits once it is shrunk produces no warning. One that does not fit even at the limit is set at the
|
|
184
|
+
limit and reported with `even at`.
|
|
185
|
+
- `renderDeck` returns what it shrank:
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
const { pptx, warnings, shrunk } = renderDeck(template, deck, { assets });
|
|
189
|
+
// shrunk: [ { slide: 3, slot: 'title', size: 32, scale: 0.941 } ]
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
- The planner knows the setting too: a draft that fits after shrinking is not sent back to the model.
|
|
193
|
+
- Without fonts the setting does nothing, because nothing can be measured.
|
|
194
|
+
|
|
195
|
+
`shrinkText` works together with `textScale`: body text is first enlarged by the template's and the deck's
|
|
196
|
+
scale, and a text that then does not fit is reduced from that size.
|
|
197
|
+
|
|
198
|
+
## The studio: checking and setting budgets
|
|
199
|
+
|
|
200
|
+
### `checkManifest` tries budgets out
|
|
201
|
+
|
|
202
|
+
For a template with fonts, `checkManifest(template)` fills each text slot with its own sample words up to the
|
|
203
|
+
slot's budget and measures the result. A budget that lets text run over is reported as a warning:
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
{ severity: 'warning', pattern: 'text-only', slot: 'body',
|
|
207
|
+
message: 'has a budget of 900 characters, but sample text of that length takes 14 lines where 9 fit; about 560 characters fit' }
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Typefaces that were not supplied are reported once, without a pattern:
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
{ severity: 'warning',
|
|
214
|
+
message: 'no font was supplied for Cambria bold; text set in it is checked by its character budget' }
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
The check is made at the designed size: `shrinkText` is not taken into account, since a budget that only fits
|
|
218
|
+
when shrunk would shrink every full slide.
|
|
219
|
+
|
|
220
|
+
### Budgets from measurement
|
|
221
|
+
|
|
222
|
+
`measuredBudgets(template)` measures how many characters each text slot's box holds, and suggests a budget a
|
|
223
|
+
little under that:
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
import { changeManifest, measuredBudgets } from 'slotdeck';
|
|
227
|
+
|
|
228
|
+
const budgets = measuredBudgets(template);
|
|
229
|
+
// [ { pattern: 'cover', slot: 'title', maxChars: 36, fits: 52, suggested: 46 }, ... ]
|
|
230
|
+
|
|
231
|
+
const { manifest } = changeManifest(
|
|
232
|
+
template,
|
|
233
|
+
budgets.map((budget) => ({ op: 'setSlot', pattern: budget.pattern, slot: budget.slot, maxChars: budget.suggested })),
|
|
234
|
+
);
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
| Field | Meaning |
|
|
238
|
+
| --- | --- |
|
|
239
|
+
| `pattern`, `slot` | Which slot. |
|
|
240
|
+
| `maxChars` | The budget the manifest states now, when it states one. |
|
|
241
|
+
| `fits` | The characters of running text the box holds, as measured. |
|
|
242
|
+
| `suggested` | `fits` less a margin: a budget to set. |
|
|
243
|
+
|
|
244
|
+
Options:
|
|
245
|
+
|
|
246
|
+
| Option | Default | Meaning |
|
|
247
|
+
| --- | --- | --- |
|
|
248
|
+
| `sample` | each slot's own sample words | Running text to measure with. Give a paragraph in the language your decks are written in when that is not the template's language: Hebrew letters are not as wide as Latin ones, so the same box holds a different number of them. |
|
|
249
|
+
| `margin` | `0.9` | The share of `fits` to suggest. Lines break between words, so two texts of the same length do not always take the same number of lines. |
|
|
250
|
+
|
|
251
|
+
A slot with several paragraphs is measured with as many paragraphs as it allows (`maxParagraphs`), which is
|
|
252
|
+
the case that leaves least room. Slots set in a typeface that was not supplied are left out. Budgets drafted
|
|
253
|
+
without fonts are deliberately cautious, so measured ones are usually larger: on the demo template by about a
|
|
254
|
+
half.
|
|
255
|
+
|
|
256
|
+
The budget is what the planning model is told, so setting measured budgets is what lets the model use the room
|
|
257
|
+
there is. Validation by measurement works with any budgets.
|
|
258
|
+
|
|
259
|
+
### A sample deck filled to the boxes
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
const { deck, assets } = sampleDeck(template, { fit: 'box' });
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
With `fit: 'box'`, each text slot is filled with as much sample text as its box holds by measurement, whatever
|
|
266
|
+
its budget says. Rendering it shows every box at its true capacity. `fill` still applies (0.95 by default) as a
|
|
267
|
+
share of what the box holds. A slot that cannot be measured is filled to its budget, as is every slot when the
|
|
268
|
+
template has no fonts.
|
|
269
|
+
|
|
270
|
+
## Measuring a text yourself
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
import { textMeasurer } from 'slotdeck';
|
|
274
|
+
|
|
275
|
+
const measure = textMeasurer(template); // or textMeasurer(template, fonts)
|
|
276
|
+
const fit = measure.fit('text-and-photo', 'body', ['First paragraph.', 'Second paragraph.']);
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
`fit(pattern, slot, text, options?)` takes a pattern id and a slot name (or the `PatternDef` and `TextSlotDef`
|
|
280
|
+
themselves), and the text as a string or one string per paragraph. It returns `undefined` when the slot cannot
|
|
281
|
+
be measured: it is not a text slot, its shape holds no text, or a typeface it needs was not supplied.
|
|
282
|
+
|
|
283
|
+
| Field of the result | Meaning |
|
|
284
|
+
| --- | --- |
|
|
285
|
+
| `fits` | Whether the text stays inside its box. |
|
|
286
|
+
| `lines` | Lines the text takes, over all its paragraphs. |
|
|
287
|
+
| `capacity` | Lines of this text the box holds. |
|
|
288
|
+
| `overflow` | Lines that do not fit; `0` when the text fits. |
|
|
289
|
+
| `height`, `room` | Height the text takes, and the height the box gives it, in points. |
|
|
290
|
+
| `widthRatio` | For a box that does not wrap: the longest line's width divided by the box's. `1` or less fits. Always `1` for a box that wraps. |
|
|
291
|
+
| `size` | Font size of the first paragraph as measured, in points. |
|
|
292
|
+
| `sizes` | The size of each paragraph, in points. |
|
|
293
|
+
| `scale` | The factor the text was reduced by to make it fit; `1` when it was not. |
|
|
294
|
+
| `fitsChars` | About how many characters of text like this the box holds. |
|
|
295
|
+
| `paragraphs` | The lines of each paragraph, as they break. |
|
|
296
|
+
| `widths` | The width of each of those lines in points, without the space that hangs at its end. |
|
|
297
|
+
| `lineRoom` | The width a line of each paragraph may take, in points. |
|
|
298
|
+
| `sampleLines` | Lines the template's own sample text takes in the slot as drawn; `0` for a slot that resizes its box or resets its size. |
|
|
299
|
+
| `approximate` | A typeface, a weight or a shape outline was stood in for, so the figures are close but not exact. |
|
|
300
|
+
|
|
301
|
+
Options:
|
|
302
|
+
|
|
303
|
+
| Option | Meaning |
|
|
304
|
+
| --- | --- |
|
|
305
|
+
| `textScale` | The factor on body text the deck is rendered with: the manifest's `textScale` times the deck's. Defaults to `1`. |
|
|
306
|
+
| `shrink` | `{ to, minSize? }`: allow the text to be set smaller, as `shrinkText` does. |
|
|
307
|
+
| `items` | For a slot of a row of items: how many items the slide has. In a row with `widen`, fewer items are wider, and the text is measured in the wider box. |
|
|
308
|
+
|
|
309
|
+
`measure.missingFonts()` returns the typefaces the template's text slots need that were not supplied.
|
|
310
|
+
|
|
311
|
+
## How a text is measured
|
|
312
|
+
|
|
313
|
+
The rules below were read off PowerPoint's own behaviour.
|
|
314
|
+
|
|
315
|
+
**The box.** The room is the shape's size less its text insets (0.1 inch left and right, 0.05 inch top and
|
|
316
|
+
bottom unless the shape says otherwise). A rounded rectangle, an ellipse or a diamond keeps text clear of its
|
|
317
|
+
outline, as PowerPoint does. A box that was drawn around its sample text holds at least what the sample takes.
|
|
318
|
+
A slot with its own `box` or `fontSize` (a derived pattern) is measured in that box at that size. A `box` of
|
|
319
|
+
the shape's own size, such as a mirrored pattern's, only moves the shape, and the sample still counts.
|
|
320
|
+
|
|
321
|
+
**Rows that widen.** A row with `widen` makes its items wider when a slide has fewer of them than the template
|
|
322
|
+
draws (see [layout-freedom.md](layout-freedom.md)). With `items` in the options, the text of such an item is
|
|
323
|
+
measured in the box the renderer gives it: the same step that lays the row out for a deck is run on a copy of
|
|
324
|
+
the slide, and the width is read from there. Only the width changes. The height of the box, and how full the
|
|
325
|
+
designer drew it (`sampleLines`), are those of the item as drawn. `validateDeck`, `renderDeck`, shrinking and
|
|
326
|
+
`checkDesign` pass the slide's `items` themselves. On the demo template, 22 texts in widened cards and figures
|
|
327
|
+
broke every line where PowerPoint did, in English and in Hebrew; measured in the boxes as drawn, 3 of the 22
|
|
328
|
+
English ones would have had PowerPoint's line count.
|
|
329
|
+
|
|
330
|
+
**The size.** The size the text will be rendered at: the template run's size or the slot's `fontSize`, times
|
|
331
|
+
the body text scale where it applies (`scales`), rounded to half a point as the renderer rounds it.
|
|
332
|
+
|
|
333
|
+
**The typeface.** As the template states it on the run that supplies the formatting (`styleRun`), or as the
|
|
334
|
+
run inherits it: from the shape's list style, its layout and master placeholders, the master's text styles,
|
|
335
|
+
the presentation's defaults, and finally the theme's fonts.
|
|
336
|
+
|
|
337
|
+
**Which typeface a character is set in.** A run names up to three typefaces: Latin, complex script (Hebrew,
|
|
338
|
+
Arabic, Thai and others) and East Asian. Hebrew letters are set in the complex-script typeface, not the Latin
|
|
339
|
+
one. When the template names none, the theme decides, and a theme often sends Hebrew to Arial or Times New
|
|
340
|
+
Roman whatever the Latin font is. In a paragraph that starts with a right-to-left letter, digits are set in
|
|
341
|
+
the complex-script typeface too, and a space or punctuation mark in the typeface of the character before it.
|
|
342
|
+
In other paragraphs digits, spaces and punctuation are set in the Latin typeface.
|
|
343
|
+
|
|
344
|
+
**Width.** The sum of the glyph widths from the font file, after the font's own shaping (Arabic joining,
|
|
345
|
+
ligatures), with each glyph rounded to an eighth of a point. PowerPoint lays text out on that grid, and at
|
|
346
|
+
small sizes the rounding changes a line's width by a percent or two, which is enough to move a word to the
|
|
347
|
+
next line. Character spacing set on the run is added. Text set in capitals by the template is measured in
|
|
348
|
+
capitals.
|
|
349
|
+
|
|
350
|
+
**Kerning.** Hebrew and other complex-script letters are always kerned with each other, at any size, and
|
|
351
|
+
never with the punctuation, digits or spaces next to them. Latin text is kerned when the template turns
|
|
352
|
+
kerning on for its size (commonly from 12 pt up), but not inside a paragraph that starts with a right-to-left
|
|
353
|
+
letter.
|
|
354
|
+
|
|
355
|
+
**Line breaks.** Words go on a line while they fit. A line may break at a space and after a hyphen or a dash;
|
|
356
|
+
not at a slash, a full stop, a colon or a Hebrew maqaf. A word longer than a whole line is cut where the line
|
|
357
|
+
ends. Chinese and Japanese text breaks between any two characters. A hanging indent or a first-line indent
|
|
358
|
+
changes the first line's width. The space at the end of a line hangs outside the box and needs no room,
|
|
359
|
+
except when it sits between two words that both run against the paragraph (two Hebrew words in an English
|
|
360
|
+
paragraph, or the reverse): then the space stays inside the line.
|
|
361
|
+
|
|
362
|
+
**Height.** Lines are 1.2 times the font size apart, whatever the font: this is how PowerPoint spaces lines.
|
|
363
|
+
Line spacing set on the paragraph (a percentage, or exact points) is applied, and so is the space between
|
|
364
|
+
paragraphs, including a slot's `paragraphGap`. The space before the first paragraph and after the last is not
|
|
365
|
+
drawn and not counted.
|
|
366
|
+
|
|
367
|
+
**Fitting.** A text fits when its height is within the room, with a slack of three tenths of a line: a last
|
|
368
|
+
line that hangs that little under the box is not reported. A box that does not wrap fits when its longest
|
|
369
|
+
line is no wider than the box.
|
|
370
|
+
|
|
371
|
+
## How close it is to PowerPoint
|
|
372
|
+
|
|
373
|
+
Checked on this project's demo template against PowerPoint 16 (build 17932) on Windows, which was asked through
|
|
374
|
+
its automation interface for the lines of every text box:
|
|
375
|
+
|
|
376
|
+
- **At the designed sizes (12 to 84 pt): 1,153 text slots in 17 decks.** The two example decks, ten decks that
|
|
377
|
+
fill every slot with running text from half its budget to 1.7 times it in English and in Hebrew, four of
|
|
378
|
+
those at text scale 1.25 and 1.5, and the studio's sample deck. In every slot, every line broke at the same
|
|
379
|
+
word as in PowerPoint, and the text height was the same to within 0.6 pt.
|
|
380
|
+
- **With every font size halved (body text of 6 to 10 pt): 700 slots in 10 decks.** 699 had PowerPoint's
|
|
381
|
+
number of lines and 697 broke every line at the same word. The three others are lines whose width equals the
|
|
382
|
+
width of their box to within an eighth of a point, where PowerPoint's own rounding of the box decides.
|
|
383
|
+
- **Shrinking:** in three decks rendered with `shrinkText` (210 slots, 108 of them shrunk), PowerPoint showed
|
|
384
|
+
every text at the size the measurer chose, in the number of lines it predicted.
|
|
385
|
+
- **Inherited styles:** a slide whose title and bulleted body state nothing themselves and take size,
|
|
386
|
+
typeface, position, bullets and indents from the layout and master (44 pt, 32 pt and a second level at
|
|
387
|
+
28 pt) gave the same lines and the same heights as PowerPoint.
|
|
388
|
+
- **With the open stand-ins** (Carlito and Caladea) the English decks gave PowerPoint's line counts in every
|
|
389
|
+
slot at the designed sizes and in 278 of 280 with halved sizes. About one slot in ten had a word on the
|
|
390
|
+
other side of a break: the stand-ins match the widths closely, not exactly.
|
|
391
|
+
|
|
392
|
+
Six of these decks are kept in the repository with the line breaks PowerPoint made, and the test suite
|
|
393
|
+
compares against them on every run: exactly with the template's own fonts where they are installed, and by
|
|
394
|
+
line count with the open stand-ins everywhere.
|
|
395
|
+
|
|
396
|
+
The demo template is set in Calibri and Cambria. Expect agreement of the same kind for other templates when
|
|
397
|
+
the font files are the ones PowerPoint uses, and treat the following as less certain:
|
|
398
|
+
|
|
399
|
+
- **Fonts that differ.** The measurement is only as good as the files. PowerPoint on a machine that lacks the
|
|
400
|
+
template's font substitutes another, and lines then break elsewhere, whatever was measured.
|
|
401
|
+
- **Tables and charts** are not measured; they keep their character budgets.
|
|
402
|
+
- **Vertical text, text in columns, and outlines other than rectangles, rounded rectangles, ellipses and
|
|
403
|
+
diamonds** are measured roughly and marked `approximate`.
|
|
404
|
+
- **PowerPoint's own autofit.** A box set to "shrink text on overflow" stores the scale PowerPoint last
|
|
405
|
+
applied; the measurer uses that stored scale. PowerPoint works a new one out only when the text is edited
|
|
406
|
+
there.
|
|
407
|
+
- **Several styles in one paragraph.** A slot's text is rendered in one run's style (`styleRun`), and that is
|
|
408
|
+
what is measured.
|
|
409
|
+
- **Line breaks by dictionary** (Thai, Khmer) and East Asian punctuation rules are simplified.
|
|
410
|
+
|
|
411
|
+
## Using another font library
|
|
412
|
+
|
|
413
|
+
`fontkitMetrics` is one implementation of a small interface. Implement it over any library:
|
|
414
|
+
|
|
415
|
+
```ts
|
|
416
|
+
import type { FontFace, FontMetrics } from 'slotdeck';
|
|
417
|
+
|
|
418
|
+
const fonts: FontMetrics = {
|
|
419
|
+
// the face for a typeface name in a weight and slant, or undefined when you do not have it
|
|
420
|
+
face(typeface, style): FontFace | undefined {
|
|
421
|
+
const font = myLibrary.find(typeface, style.bold, style.italic);
|
|
422
|
+
if (!font) return undefined;
|
|
423
|
+
return {
|
|
424
|
+
has: (codePoint) => font.hasGlyph(codePoint),
|
|
425
|
+
// the advance of each glyph of the text, in ems: the library's units divided by the font's units per em
|
|
426
|
+
advances: (text, kerning) => font.shape(text, { kerning }).map((glyph) => glyph.advance / font.unitsPerEm),
|
|
427
|
+
};
|
|
428
|
+
},
|
|
429
|
+
// optional: a face for a character the chosen face lacks
|
|
430
|
+
fallback(codePoint, style) {
|
|
431
|
+
return undefined;
|
|
432
|
+
},
|
|
433
|
+
};
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
`advances` is called with a piece of text in one script and one typeface (a word, the letters of a Hebrew
|
|
437
|
+
word, or the space between two words), so the library should shape it: for Arabic, the joined forms have other
|
|
438
|
+
widths than the letters alone. It returns one number for each glyph rather than a total, because slotdeck
|
|
439
|
+
rounds each glyph the way PowerPoint does. With `kerning` true, the kerning between two glyphs belongs in the
|
|
440
|
+
advance of the first. Set `substitute: true` on a face that stands in for another weight or slant; results
|
|
441
|
+
that use it are marked `approximate`.
|
|
442
|
+
|
|
443
|
+
## Checking against PowerPoint yourself
|
|
444
|
+
|
|
445
|
+
The repository has the scripts the figures above were made with. They need Windows with PowerPoint and are not
|
|
446
|
+
part of the package.
|
|
447
|
+
|
|
448
|
+
```
|
|
449
|
+
node scripts/text-fit-stress.ts # writes out/stress-*.pptx and their deck JSON
|
|
450
|
+
powershell -File scripts/measure-pptx.ps1 -Path out/stress-he-3.pptx -Out out/stress-he-3.lines.json
|
|
451
|
+
node scripts/compare-text-fit.ts out/stress-he-3.deck.json out/stress-he-3.lines.json
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
`measure-pptx.ps1` writes, for every text box of a pptx, the lines PowerPoint breaks its text into, the width
|
|
455
|
+
of each line and the height the text takes. `compare-text-fit.ts` prints every slot where the measurer
|
|
456
|
+
disagrees. `text-fit-stress.ts --small` writes the decks with halved font sizes.
|
package/docs/troubleshooting.md
CHANGED
|
@@ -43,6 +43,11 @@ These do not stop rendering. `renderDeck` and the planner return them.
|
|
|
43
43
|
| Message | Meaning | What to do |
|
|
44
44
|
| --- | --- | --- |
|
|
45
45
|
| `text is 230 characters; the slot fits about 211` | Text over the slot's `maxChars`. It is rendered and may overflow its box. | Shorten the text, or raise the budget in the manifest if the sample deck shows room. |
|
|
46
|
+
| `text takes 7 lines at 18 pt; the box holds 5 (2 lines too many). Text of about 290 characters fits.` | (Template loaded with fonts.) The text was measured and runs over its box by that many lines. | Shorten the text to about the length given, or allow shrinking with `shrinkText`. See [text-fit.md](text-fit.md). |
|
|
47
|
+
| `text takes 6 lines even at 15.5 pt; the box holds 5 ...` | The text was set as small as `shrinkText` allows and still does not fit. | Shorten the text. |
|
|
48
|
+
| `text is 30% wider than its box at 12 pt, and the box does not wrap. ...` | (With fonts.) A one-line box whose text is too long. | Shorten the text. |
|
|
49
|
+
| `text takes 1 line where the template's own text takes 5, so its box will look empty. ...` | (From `checkDesign` or a `review`.) The text fits but leaves its box almost empty. Also: `...so the row will look uneven`, and `the last of its 2 lines holds one word, ...`. | Lengthen or shorten the text as the message says. See [design-checks.md](design-checks.md). |
|
|
50
|
+
| `seen in the picture of the slide: ...` | (From `visionReview`.) A model looked at a picture of the slide and reported this. It is only as reliable as the picture. | Check the slide; see the cautions in [design-checks.md](design-checks.md). |
|
|
46
51
|
| `text has 5 paragraphs; the slot fits 4` | Over `maxParagraphs`. | Fewer paragraphs. |
|
|
47
52
|
| `the table has 6 columns; the slot fits 5` (also rows) | Over the table's limits. The table may leave the slide. | A smaller table. |
|
|
48
53
|
| `a cell is 40 characters; cells fit about 22` | A long cell makes its row taller. | Shorter cells. |
|
|
@@ -89,6 +94,9 @@ Reasons a pattern is not offered:
|
|
|
89
94
|
| warning: `has no character budget, so nothing stops text that does not fit` | Add `maxChars`. |
|
|
90
95
|
| warning: `points at the same shape as "x"` | Two slots on one shape: usually a mistake. |
|
|
91
96
|
| warning: `a grid cannot hold more items than it draws` | Lower `max` to the number drawn. |
|
|
97
|
+
| warning: `has a budget of 900 characters, but sample text of that length takes 14 lines where 9 fit; about 560 characters fit` | (Template loaded with fonts.) The budget lets text run over the box. Lower `maxChars` to about the length given, or use `measuredBudgets`. See [text-fit.md](text-fit.md). |
|
|
98
|
+
| warning: `has a budget of 520 characters on a slide with 2 of its 4 items, but sample text of that length takes 10 lines where 7 fit; about 376 characters fit` | (Template loaded with fonts, row with `widen`.) With that number of items the row's items are wider and their texts have a larger budget, which still lets text run over the wider box. Lower the slot's `maxChars`: the budget at each number of items follows from it. See "Fewer items that fill the row" in [layout-freedom.md](layout-freedom.md). |
|
|
99
|
+
| warning: `no font was supplied for Cambria bold; text set in it is checked by its character budget` | Add the font file to `fontkitMetrics`, or accept character budgets for those slots. |
|
|
92
100
|
|
|
93
101
|
## Refusals from `changeManifest`
|
|
94
102
|
|
|
@@ -159,7 +167,8 @@ Plain `Error`s from `renderDeck`. They mean the manifest and the pptx disagree,
|
|
|
159
167
|
as empty: a drafted budget for it is only as long as its sample text, and `box` cannot be applied to it. Decks
|
|
160
168
|
exported from Google Slides and decks built from free-standing text boxes work as they are.
|
|
161
169
|
- **Budgets are estimates.** A character budget cannot see lines lost to wrapping. Leave room in budgets for
|
|
162
|
-
slots with several paragraphs.
|
|
170
|
+
slots with several paragraphs. A template loaded with its fonts measures text in lines and does not have this
|
|
171
|
+
limit for text slots: see [text-fit.md](text-fit.md). Tables and charts are not measured.
|
|
163
172
|
- **Text boxes do not resize.** Text longer than its box overflows.
|
|
164
173
|
- **Charts:** `column`, `bar`, `line` and `pie` only. No stacked or combined charts.
|
|
165
174
|
- **Tables:** the sample table must not have merged cells.
|
|
@@ -168,7 +177,9 @@ Plain `Error`s from `renderDeck`. They mean the manifest and the pptx disagree,
|
|
|
168
177
|
- **Animations** on template slides were not tested. A slide whose animated shape is removed may make PowerPoint
|
|
169
178
|
offer to repair the file; use templates without animations, or keep animated shapes out of optional slots.
|
|
170
179
|
- **Not read back:** changes made to a rendered file in PowerPoint. The deck JSON is the source of truth.
|
|
171
|
-
- **Not possible:**
|
|
180
|
+
- **Not possible:** positioning a shape from a deck; mirroring a slide because of a deck's reading direction
|
|
181
|
+
(a mirrored layout is a separate pattern: see "Variants the code works out" in
|
|
182
|
+
[layout-freedom.md](layout-freedom.md)).
|
|
172
183
|
- **What was tested.** Output was checked by rendering in PowerPoint on Windows. Other programs that open pptx
|
|
173
184
|
files were not tested. The Gemini adapters were run against the Gemini Developer API; the Vertex AI endpoint
|
|
174
185
|
is supported but was not run live.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slotdeck",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Turn a PowerPoint template into new decks: a model plans the slides, the code clones the template's own slides and fills them.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pptx",
|
|
@@ -60,7 +60,13 @@
|
|
|
60
60
|
"fflate": "^0.8.3"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
|
+
"@expo-google-fonts/caladea": "^0.4.2",
|
|
64
|
+
"@expo-google-fonts/carlito": "^0.4.1",
|
|
65
|
+
"@expo-google-fonts/heebo": "^0.4.2",
|
|
66
|
+
"@pagus-kit/core": "^0.1.1",
|
|
67
|
+
"@pagus-kit/renderer": "^0.1.1",
|
|
63
68
|
"@types/node": "^26.6.6",
|
|
69
|
+
"fontkit": "^3.0.0",
|
|
64
70
|
"typescript": "^7.0.2"
|
|
65
71
|
}
|
|
66
72
|
}
|