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.
Files changed (104) hide show
  1. package/README.md +86 -6
  2. package/dist/deck/alternatives.d.ts +34 -0
  3. package/dist/deck/alternatives.js +370 -0
  4. package/dist/deck/alternatives.js.map +1 -0
  5. package/dist/deck/design.d.ts +40 -0
  6. package/dist/deck/design.js +112 -0
  7. package/dist/deck/design.js.map +1 -0
  8. package/dist/deck/validate.d.ts +18 -0
  9. package/dist/deck/validate.js +38 -2
  10. package/dist/deck/validate.js.map +1 -1
  11. package/dist/index.d.ts +22 -1
  12. package/dist/index.js +13 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/node.d.ts +7 -0
  15. package/dist/node.js +29 -0
  16. package/dist/node.js.map +1 -1
  17. package/dist/plan/catalog.js +3 -1
  18. package/dist/plan/catalog.js.map +1 -1
  19. package/dist/plan/edit.js +6 -2
  20. package/dist/plan/edit.js.map +1 -1
  21. package/dist/plan/plan.d.ts +10 -0
  22. package/dist/plan/plan.js +8 -0
  23. package/dist/plan/plan.js.map +1 -1
  24. package/dist/plan/review.d.ts +43 -0
  25. package/dist/plan/review.js +141 -0
  26. package/dist/plan/review.js.map +1 -0
  27. package/dist/plan/write.d.ts +5 -0
  28. package/dist/plan/write.js +11 -4
  29. package/dist/plan/write.js.map +1 -1
  30. package/dist/preview/pagus.d.ts +30 -0
  31. package/dist/preview/pagus.js +45 -0
  32. package/dist/preview/pagus.js.map +1 -0
  33. package/dist/preview/patterns.d.ts +27 -0
  34. package/dist/preview/patterns.js +35 -0
  35. package/dist/preview/patterns.js.map +1 -0
  36. package/dist/preview/provider.d.ts +30 -0
  37. package/dist/preview/provider.js +11 -0
  38. package/dist/preview/provider.js.map +1 -0
  39. package/dist/render/render.d.ts +18 -1
  40. package/dist/render/render.js +28 -8
  41. package/dist/render/render.js.map +1 -1
  42. package/dist/render/repeat.d.ts +17 -2
  43. package/dist/render/repeat.js +150 -4
  44. package/dist/render/repeat.js.map +1 -1
  45. package/dist/render/text.d.ts +6 -0
  46. package/dist/render/text.js +24 -0
  47. package/dist/render/text.js.map +1 -1
  48. package/dist/template/budgets.d.ts +33 -0
  49. package/dist/template/budgets.js +38 -0
  50. package/dist/template/budgets.js.map +1 -0
  51. package/dist/template/change.d.ts +18 -1
  52. package/dist/template/change.js +33 -3
  53. package/dist/template/change.js.map +1 -1
  54. package/dist/template/check.d.ts +3 -0
  55. package/dist/template/check.js +53 -3
  56. package/dist/template/check.js.map +1 -1
  57. package/dist/template/derive.d.ts +54 -0
  58. package/dist/template/derive.js +402 -0
  59. package/dist/template/derive.js.map +1 -0
  60. package/dist/template/draft.d.ts +7 -0
  61. package/dist/template/draft.js +17 -1
  62. package/dist/template/draft.js.map +1 -1
  63. package/dist/template/instruct.js +11 -5
  64. package/dist/template/instruct.js.map +1 -1
  65. package/dist/template/manifest.d.ts +22 -0
  66. package/dist/template/manifest.js +17 -1
  67. package/dist/template/manifest.js.map +1 -1
  68. package/dist/template/sample.d.ts +17 -0
  69. package/dist/template/sample.js +27 -2
  70. package/dist/template/sample.js.map +1 -1
  71. package/dist/text/fontkit.d.ts +18 -0
  72. package/dist/text/fontkit.js +91 -0
  73. package/dist/text/fontkit.js.map +1 -0
  74. package/dist/text/fonts.d.ts +45 -0
  75. package/dist/text/fonts.js +59 -0
  76. package/dist/text/fonts.js.map +1 -0
  77. package/dist/text/layout.d.ts +12 -0
  78. package/dist/text/layout.js +104 -0
  79. package/dist/text/layout.js.map +1 -0
  80. package/dist/text/measure.d.ts +85 -0
  81. package/dist/text/measure.js +369 -0
  82. package/dist/text/measure.js.map +1 -0
  83. package/dist/text/styles.d.ts +123 -0
  84. package/dist/text/styles.js +417 -0
  85. package/dist/text/styles.js.map +1 -0
  86. package/dist/tools.d.ts +12 -0
  87. package/dist/tools.js +32 -6
  88. package/dist/tools.js.map +1 -1
  89. package/docs/README.md +15 -5
  90. package/docs/api-reference.md +278 -8
  91. package/docs/changelog.md +135 -0
  92. package/docs/chat-tools.md +22 -5
  93. package/docs/deck-format.md +6 -0
  94. package/docs/design-checks.md +216 -0
  95. package/docs/generating.md +4 -1
  96. package/docs/layout-freedom.md +255 -3
  97. package/docs/manifest-format.md +19 -3
  98. package/docs/preview.md +217 -0
  99. package/docs/providers.md +1 -1
  100. package/docs/rendering.md +8 -0
  101. package/docs/template-studio.md +110 -11
  102. package/docs/text-fit.md +456 -0
  103. package/docs/troubleshooting.md +13 -2
  104. package/package.json +7 -1
@@ -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.
@@ -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:** swapping the side of a photo; positioning a shape from a deck.
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.1.2",
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
  }