slotdeck 0.1.1 → 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 (109) hide show
  1. package/README.md +94 -8
  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 +44 -6
  10. package/dist/deck/validate.js.map +1 -1
  11. package/dist/index.d.ts +23 -1
  12. package/dist/index.js +14 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/llm/gemini.d.ts +4 -0
  15. package/dist/llm/gemini.js +43 -7
  16. package/dist/llm/gemini.js.map +1 -1
  17. package/dist/llm/provider.d.ts +12 -0
  18. package/dist/node.d.ts +7 -0
  19. package/dist/node.js +29 -0
  20. package/dist/node.js.map +1 -1
  21. package/dist/plan/catalog.d.ts +11 -1
  22. package/dist/plan/catalog.js +23 -8
  23. package/dist/plan/catalog.js.map +1 -1
  24. package/dist/plan/edit.js +14 -6
  25. package/dist/plan/edit.js.map +1 -1
  26. package/dist/plan/plan.d.ts +26 -2
  27. package/dist/plan/plan.js +58 -17
  28. package/dist/plan/plan.js.map +1 -1
  29. package/dist/plan/review.d.ts +43 -0
  30. package/dist/plan/review.js +141 -0
  31. package/dist/plan/review.js.map +1 -0
  32. package/dist/plan/write.d.ts +13 -1
  33. package/dist/plan/write.js +40 -7
  34. package/dist/plan/write.js.map +1 -1
  35. package/dist/preview/pagus.d.ts +30 -0
  36. package/dist/preview/pagus.js +45 -0
  37. package/dist/preview/pagus.js.map +1 -0
  38. package/dist/preview/patterns.d.ts +27 -0
  39. package/dist/preview/patterns.js +35 -0
  40. package/dist/preview/patterns.js.map +1 -0
  41. package/dist/preview/provider.d.ts +30 -0
  42. package/dist/preview/provider.js +11 -0
  43. package/dist/preview/provider.js.map +1 -0
  44. package/dist/render/render.d.ts +18 -1
  45. package/dist/render/render.js +32 -12
  46. package/dist/render/render.js.map +1 -1
  47. package/dist/render/repeat.d.ts +17 -2
  48. package/dist/render/repeat.js +150 -4
  49. package/dist/render/repeat.js.map +1 -1
  50. package/dist/render/text.d.ts +6 -0
  51. package/dist/render/text.js +24 -0
  52. package/dist/render/text.js.map +1 -1
  53. package/dist/template/budgets.d.ts +33 -0
  54. package/dist/template/budgets.js +38 -0
  55. package/dist/template/budgets.js.map +1 -0
  56. package/dist/template/change.d.ts +19 -1
  57. package/dist/template/change.js +45 -3
  58. package/dist/template/change.js.map +1 -1
  59. package/dist/template/check.d.ts +3 -0
  60. package/dist/template/check.js +53 -3
  61. package/dist/template/check.js.map +1 -1
  62. package/dist/template/derive.d.ts +54 -0
  63. package/dist/template/derive.js +402 -0
  64. package/dist/template/derive.js.map +1 -0
  65. package/dist/template/draft.d.ts +7 -0
  66. package/dist/template/draft.js +17 -1
  67. package/dist/template/draft.js.map +1 -1
  68. package/dist/template/instruct.js +15 -7
  69. package/dist/template/instruct.js.map +1 -1
  70. package/dist/template/manifest.d.ts +38 -0
  71. package/dist/template/manifest.js +43 -1
  72. package/dist/template/manifest.js.map +1 -1
  73. package/dist/template/sample.d.ts +17 -0
  74. package/dist/template/sample.js +31 -5
  75. package/dist/template/sample.js.map +1 -1
  76. package/dist/text/fontkit.d.ts +18 -0
  77. package/dist/text/fontkit.js +91 -0
  78. package/dist/text/fontkit.js.map +1 -0
  79. package/dist/text/fonts.d.ts +45 -0
  80. package/dist/text/fonts.js +59 -0
  81. package/dist/text/fonts.js.map +1 -0
  82. package/dist/text/layout.d.ts +12 -0
  83. package/dist/text/layout.js +104 -0
  84. package/dist/text/layout.js.map +1 -0
  85. package/dist/text/measure.d.ts +85 -0
  86. package/dist/text/measure.js +369 -0
  87. package/dist/text/measure.js.map +1 -0
  88. package/dist/text/styles.d.ts +123 -0
  89. package/dist/text/styles.js +417 -0
  90. package/dist/text/styles.js.map +1 -0
  91. package/dist/tools.d.ts +12 -0
  92. package/dist/tools.js +38 -6
  93. package/dist/tools.js.map +1 -1
  94. package/docs/README.md +16 -5
  95. package/docs/api-reference.md +291 -9
  96. package/docs/changelog.md +242 -0
  97. package/docs/chat-tools.md +26 -5
  98. package/docs/deck-format.md +6 -0
  99. package/docs/design-checks.md +216 -0
  100. package/docs/generating.md +43 -4
  101. package/docs/layout-freedom.md +278 -3
  102. package/docs/manifest-format.md +20 -3
  103. package/docs/preview.md +217 -0
  104. package/docs/providers.md +49 -4
  105. package/docs/rendering.md +8 -0
  106. package/docs/template-studio.md +121 -12
  107. package/docs/text-fit.md +456 -0
  108. package/docs/troubleshooting.md +21 -2
  109. package/package.json +7 -1
@@ -34,6 +34,8 @@ What the renderer does:
34
34
  - **Spacing:** the items are laid out again around the centre of the area the row occupied. They keep the
35
35
  template's spacing when there is room and move closer together when there is not. With a `span`, the row may
36
36
  spread out to those limits.
37
+ - **Size:** every item keeps its drawn size, unless the row has `widen` (see "Fewer items that fill the row"
38
+ below).
37
39
  - Without `items`, the slide has the number of items drawn on the template slide.
38
40
 
39
41
  An `items` value outside `min` to `max` is an error:
@@ -42,6 +44,57 @@ An `items` value outside `min` to `max` is an error:
42
44
  How many items fit is a property of the template: `changeManifest` refuses a `max` that does not fit side by
43
45
  side and says the most that do (see [template-studio.md](template-studio.md)).
44
46
 
47
+ ### Fewer items that fill the row
48
+
49
+ By default an item keeps its drawn size, so three cards in a row drawn for four stand in the middle with room
50
+ left at both ends. With `widen` on the `repeat`, a line that holds fewer items than the template draws has its
51
+ items made wider, so that they fill the row:
52
+
53
+ ```json
54
+ "repeat": {
55
+ "item": "card",
56
+ "slots": ["card{n}Icon", "card{n}Heading", "card{n}Text"],
57
+ "shapes": [["6"], ["12"], ["18"], ["24"]],
58
+ "min": 2,
59
+ "max": 4,
60
+ "widen": 2.2
61
+ }
62
+ ```
63
+
64
+ `widen` is the most an item may grow, as a factor on its drawn width: `1.5` lets a card become half as wide
65
+ again, `2.2` lets two cards fill a row drawn for four. With `changeManifest`:
66
+ `{ op: 'setRepeat', pattern: 'icon-cards', widen: 2.2 }`; `null` or `1` turns it off.
67
+
68
+ - **How wide.** The items share the row's span (`span`, or the row as drawn) and keep the template's own gap
69
+ between them. Where that is more than the limit allows, they stop at the limit and the row is centred. In
70
+ the demo template a card is 21% of the slide wide; three become 29% each and two become 45%.
71
+ - **What decides.** Only the number of items. Long text does not widen a card and short text does not narrow
72
+ one, so the same deck always looks the same.
73
+ - **Inside an item** each shape is treated on its own, whether the item is a group or not:
74
+ - A background, a rule or a text box that spans the item grows with it and keeps its distance to both
75
+ sides. The text boxes of the row's own slots always grow.
76
+ - A picture or an icon keeps its size. So does a circle or any other outline that is not a rectangle, and
77
+ a slanted line. It keeps its distance to the nearer side of the item: an icon at the start of a card
78
+ stays at the start, and one in the middle stays in the middle.
79
+ - Rounded and cut corners keep the size they were drawn with.
80
+ - Heights do not change.
81
+ - **Budgets.** A wider text box holds more. The character budget of the row's text slots grows by the factor
82
+ the items are at least wider by: the number drawn on a line divided by the number held, up to `widen`. In a
83
+ row drawn for four with `widen: 2.2`, a 117-character text holds 156 with three cards and 234 with two.
84
+ `validateDeck` uses these budgets, the planner gives them to the model for the number of items it chose,
85
+ and `sampleDeck` with `items: 'min'` fills them. Paragraph budgets do not change. A deck's text size is
86
+ applied on top.
87
+ - **With fonts.** A template loaded with fonts measures the text of a widened item in its wider box, so a
88
+ text is reported, shrunk or called empty by the box it really gets (see [text-fit.md](text-fit.md)).
89
+ `checkManifest` then also tries the larger budgets out, one number of items at a time.
90
+ - **Grids.** A grid has one item width, decided by its fullest line. In a three-column grid, four figures make
91
+ two lines of two, which widen; five make three and two, and keep their width.
92
+ - **Both reading directions.** Positions come from the template: a right-to-left template with its icons at
93
+ the right edge of each card keeps them there. A deck that reads the other way gets the same shapes, with
94
+ its first item at the other end.
95
+ - **Nothing else changes.** At the drawn number of items and above, and for every row without `widen`, the
96
+ output is byte for byte what it was.
97
+
45
98
  ## 2. Grids
46
99
 
47
100
  With `columns` in the `repeat`, the items form a grid. `columns` is how many items one line holds on the
@@ -72,6 +125,29 @@ or, on a brief for the planner, `textSize: 'large'` (1.25) or `'x-large'` (1.5).
72
125
  shows the model the reduced budgets, so it writes less.
73
126
  - Through `editDeck`, "make the text larger" sets the scale and rewrites only the slides whose text no longer
74
127
  fits.
128
+ - Table text and chart labels follow the scale where the manifest states their size (`bodyText.size`,
129
+ `headerText.size`, `text.size`), and a table cell then holds less.
130
+
131
+ ### Larger text for a whole template
132
+
133
+ The engine keeps the template's own font sizes. A template designed with small body text, such as 9 pt on a
134
+ 10-inch-wide slide, therefore gives decks with small body text. To change that once, for every deck, set
135
+ `textScale` on the manifest:
136
+
137
+ ```json
138
+ { "version": 1, "name": "corporate", "direction": "rtl", "textScale": 1.3, "patterns": [ ... ] }
139
+ ```
140
+
141
+ With `changeManifest`: `{ op: 'setManifest', textScale: 1.3 }`.
142
+
143
+ - It works exactly like a deck's `textScale` and applies to the same slots (body text: 12 pt and under by
144
+ default), but for every deck made from the template.
145
+ - Budgets shrink to match, in validation, in what the planner shows the model, and in `sampleDeck`: a slot
146
+ that held 240 characters holds 142 at 1.3. Render the sample deck after setting it to see that everything
147
+ still fits.
148
+ - A deck's own text size goes on top. With a template at 1.3, a deck at `large` (1.25) shows body text at about
149
+ 1.6 times the designed size.
150
+ - A slot that should keep its size can opt out with `"scales": false`.
75
151
 
76
152
  ## 4. Derived patterns
77
153
 
@@ -102,12 +178,211 @@ Example: a text-only pattern derived from a photo pattern.
102
178
  }
103
179
  ```
104
180
 
105
- With `changeManifest` this is three operations (`copyPattern`, `addSlot` with role `decoration`, `setSlot` with
106
- a `box`), and the budgets are worked out for you: see [template-studio.md](template-studio.md).
181
+ By hand, with `changeManifest`, this is three operations (`copyPattern`, `addSlot` with role `decoration`,
182
+ `setSlot` with a `box`), and the budgets are worked out for you: see [template-studio.md](template-studio.md).
183
+ For the three common variants below, the boxes are worked out as well.
184
+
185
+ ### Variants the code works out
186
+
187
+ Three kinds of derived pattern need no box typed in. One `changeManifest` operation makes each, and
188
+ `editManifest` lets a model ask for one by name:
189
+
190
+ ```ts
191
+ changeManifest(template, [
192
+ { op: 'derivePattern', pattern: 'text-and-photo', variant: 'mirror' }, // text-and-photo-mirrored
193
+ { op: 'derivePattern', pattern: 'text-and-photo', variant: 'text-only' }, // text-and-photo-text-only
194
+ { op: 'derivePattern', pattern: 'text-and-photo', variant: 'without', slot: 'subtitle' }, // text-and-photo-no-subtitle
195
+ ]);
196
+ ```
197
+
198
+ Each makes a new pattern next to its source. A deck uses it by its id like any other pattern, and the planner
199
+ chooses it by its description; nothing changes in the deck format. `patternVariants(template, id)` says which
200
+ variants a pattern allows before you ask.
201
+
202
+ **How the shapes of a slide are sorted.** The code looks at the shapes that sit directly on the slide (a group
203
+ counts as one shape) and puts each in one of these:
204
+
205
+ | What | Which shapes | In a variant |
206
+ | --- | --- | --- |
207
+ | Locked | The logo, rules and other elements that repeat across the template's slides; page-number, date and footer placeholders; texts that are deck fields. | Never moved. |
208
+ | The picture | The pattern's one photo, graphic or chart slot. Icons, pictures inside a row of items and pictures kept as decoration do not count. | Changes sides, or is left out. |
209
+ | Beside the picture | Every shape that shares no width with the picture: titles, texts, panels. | Moves as one block, so edges the shapes share stay shared. |
210
+ | With the picture | Shapes in the picture's own column: a sticker on it, a caption under it. | Goes where the picture goes. |
211
+ | Around both | A title above the picture and its text, a caption under both, a background behind both. | Stays. |
212
+
213
+ #### `mirror`
214
+
215
+ The picture and the block beside it change sides.
216
+
217
+ - They change places inside the room the two share, so the slide keeps its margins. A picture that runs off
218
+ one edge of the slide runs off the other edge instead, and the block keeps the margin it had.
219
+ - Nothing changes size, so no budget changes. The block is moved, not flipped: text boxes that shared a left
220
+ edge still share one.
221
+ - Locked shapes stay. When the picture would cover one near the top or the bottom of the slide (a full-height
222
+ photo landing on the logo), the picture is cut back to start a little under it, or end a little above it,
223
+ and the photo is cropped to the new frame.
224
+ - Shapes that are not slots, and groups, are moved too. The operation adds a slot for each, named `moved1`,
225
+ `moved2`, ..., with `"empty": "keep"` and a `box`: the shape keeps its content and is shown at the box.
226
+ - A mirrored pattern is a layout of its own that a deck or the planner picks. A deck's reading direction still
227
+ never moves a shape: a right-to-left deck made from `photo-stat` has its photo where the template drew it.
228
+
229
+ In the demo template, `text-and-photo-mirrored`, `photo-stat-mirrored` and `list-with-photo-mirrored` were made
230
+ this way.
231
+
232
+ #### `text-only`
233
+
234
+ The picture is left out and the text beside it widens.
235
+
236
+ - The picture's slot stays under its name with `"empty": "drop"`, so its shape is always removed. Decoration
237
+ drawn on the picture is removed with it. Texts the deck writes are never removed.
238
+ - Every text slot beside the picture grows towards the side the picture was on: up to the picture's far edge,
239
+ or, for a picture that ran off the slide, up to the margin the text keeps on its own side. All of them end
240
+ on the same edge.
241
+ - The column is no wider than about 90 characters a line of the slot with the largest budget, because lines
242
+ that run the width of a slide are hard to read. It keeps the edge where the template's reading starts (the
243
+ left edge of a left-to-right template) and gives up room at the other.
244
+ - A text stops short of any other shape that is in its way.
245
+ - The budget of each widened text is measured again for its new width.
246
+ - To enlarge the text of the new pattern, follow with `setSlot` and a `fontSize`; the budget follows.
247
+
248
+ #### `without`
249
+
250
+ One optional text is left out for good, and the room it took is used.
251
+
252
+ - `slot` names the text. It must be optional (`"empty": "remove"`). In the variant it has `"empty": "drop"`.
253
+ - The shapes under it in its column move up, so that the first of them starts where the text started.
254
+ - The lowest text among them keeps its lower edge, so it becomes taller and its budget grows. This is done
255
+ only when its text starts at the top of its box; a text that is centred or sits at the bottom of its box is
256
+ moved and keeps its size, so that the text does not shift inside it.
257
+ - When nothing sits under the text, the text above it is extended down to where the text ended, on the same
258
+ condition.
259
+ - When there is nothing to move or extend, the variant is the same slide with the slot gone.
260
+
261
+ Why make one when the slot is optional anyway: in `strict` layout an optional slot has to be filled, and in
262
+ `adaptive` layout leaving it empty leaves its room empty. The variant closes the gap and lets the text under it
263
+ say more.
264
+
265
+ #### When a variant is refused
266
+
267
+ `derivePattern` refuses, with the reason, when the slide does not allow the variant:
268
+
269
+ | Reason | Example |
270
+ | --- | --- |
271
+ | The pattern has no picture of its own. | `"cover" has no photo, graphic or chart of its own, so there is nothing to change sides with` |
272
+ | It has more than one. | `"gallery" has 2 pictures (photo1, photo2); this needs a slide with one picture beside its text` |
273
+ | Nothing sits beside the picture. | A wide diagram under a title: `nothing sits beside the picture "diagramGraphic", ...` |
274
+ | The picture has content on both of its sides. | A picture in the middle of the slide. |
275
+ | A shape that keeps its place is where something would go. | `shape #9 ("Source: annual report"), which keeps its place, is where shape #7 would go` |
276
+ | A row of items would have to move. | The renderer lays a row out again for every item count, so a box cannot place it. |
277
+ | A table would have to move. | A table slot has no `box`. |
278
+ | The text to widen is inside a group. | `the text beside the picture is inside a group, and a shape inside a group cannot take a box` |
279
+ | The slot to leave out is not an optional text. | `"title" is required; only an optional text can be left out; "text-and-photo" has "subtitle"` |
280
+
281
+ Render the sample deck after deriving a variant (`sampleDeck`, then `renderDeck`): it holds every pattern,
282
+ variants included, filled to its budgets.
283
+
284
+ ## Another layout for the same slide
285
+
286
+ The sections above are about what a deck may ask for. `slideAlternatives` answers another question: a slide
287
+ already has its content; which other patterns could show it? It puts the content into every pattern that has a
288
+ place for all of it and returns the slide written for each. It uses no model and changes no words, so an
289
+ alternative says exactly what the slide said.
290
+
291
+ ```ts
292
+ import { slideAlternatives } from 'slotdeck';
293
+
294
+ const alternatives = slideAlternatives(manifest, deck, 5, { layout: 'adaptive' }); // slide 5 of the deck, counted from 1
295
+ // [
296
+ // {
297
+ // pattern: 'text-only',
298
+ // description: 'A title, an optional supporting line and two or three short paragraphs, with no picture. ...',
299
+ // slide: { pattern: 'text-only', slots: { title: 'Three changes for next season', body: ['Longer hours: Bikes unlock ...', '...', '...'] } },
300
+ // changes: ['The 3 cards became a list of 3 points, each written as "heading: text".'],
301
+ // dropped: ['card1Icon', 'card2Icon', 'card3Icon'],
302
+ // warnings: [],
303
+ // },
304
+ // ]
305
+
306
+ deck.slides[4] = alternatives[0].slide; // take one, then validate and render as usual
307
+ ```
308
+
309
+ | Field | Meaning |
310
+ | --- | --- |
311
+ | `pattern`, `description` | The pattern's id and what it is for, from the manifest. |
312
+ | `slide` | The slide written for that pattern: put it in the deck in place of the original. |
313
+ | `changes` | What was done to the content where it was not copied slot for slot, a sentence each. Empty for a plain copy. |
314
+ | `dropped` | Slots of the original slide whose photo or icon the pattern has no place for. |
315
+ | `warnings` | What `validateDeck` says about the new slide, such as `text is 30 characters; the slot fits about 20`. Empty when everything fits. |
316
+
317
+ Options: `layout` (`'strict'` by default, as in `renderDeck`) and `patterns` (consider only these ids). The list
318
+ is ordered: alternatives that fit and leave nothing out come first, then those that leave out a photo or
319
+ icons, then those with a warning. A slide with no alternative gives `[]`.
320
+
321
+ ### How the content of a slide is read
322
+
323
+ A manifest does not say what a slot is for, so the code tells from names and limits:
324
+
325
+ | Part | Which slot |
326
+ | --- | --- |
327
+ | Title | The text slot whose name ends in `title` (`title`, `slideTitle`) and is not a subtitle. |
328
+ | Subtitle | The text slot whose name ends in `subtitle` or `subheading`. |
329
+ | Figure | A big, short text: a budget of at most 10 characters at 24 pt or more, such as `87%`. |
330
+ | Body | Of the text slots that are left, the one with the largest budget. It counts as a list when it takes several paragraphs (`maxParagraphs`). |
331
+ | Picture | The pattern's one photo or graphic outside its row. |
332
+ | Items | The row or grid (`repeat`). In each item: an icon, a picture, a figure, and of its other texts the one with the smaller budget is the heading and the one with the larger is the text. |
333
+ | Everything else | A takeaway, a footnote, a table, a chart. It keeps its name and fits only a slot of the same name and kind. |
334
+
335
+ Deck fields and slots with `"empty": "keep"` or `"drop"` take no part.
336
+
337
+ ### What can become what
338
+
339
+ - **The same part.** A title goes to the title, a subtitle to the subtitle, a figure to the figure, the body to
340
+ the body, a picture to a picture of the same kind.
341
+ - **Variants of one slide.** Between a pattern and its mirror image, or any two patterns whose slots have the
342
+ same names, every value goes to the slot of the same name.
343
+ - **Items to items.** Cards become columns, a row becomes a grid, when the target takes that many items.
344
+ Headings go to headings and texts to texts. Items with a heading and a text fit items with one text, written
345
+ `heading: text`; single texts written that way fit items with a heading and a text, split at the first colon
346
+ (or dash between spaces).
347
+ - **Items to a list.** One point for each item, written `heading: text`, in a body that takes several
348
+ paragraphs. Each part of an item must be one paragraph.
349
+ - **A list to items.** One item for each point, from two points up. For items with a heading and a text, only
350
+ when every point is written `heading: text`.
351
+ - **A list to paragraphs and back.** The words are the same; whether they show as points is the new slot's own
352
+ formatting. `changes` says so.
353
+
354
+ ### When a pattern is an alternative
355
+
356
+ - **Everything the slide says has a place**: every text, table, chart and supplied graphic. Nothing is cut and
357
+ nothing is run together, apart from `heading: text`.
358
+ - **Everything the pattern requires can be filled** from the slide. In `strict` layout that includes its
359
+ optional slots; in `adaptive` layout those may stay empty. Code does not invent a subtitle, a heading, a
360
+ number, an icon or a photo, so a pattern that needs one the slide lacks is not offered.
361
+ - **A photo or an icon may be left out**, because it illustrates. `dropped` names the slots.
362
+ - **A figure stays a figure.** Figures do not become headings of cards, and headings do not become figures.
363
+ - **A text that is too long for its new slot does not rule a pattern out.** It is reported in `warnings`, and
364
+ the alternative is listed after those that fit.
365
+
366
+ ### Limits
367
+
368
+ - It judges what fits, not what suits. A section slide is offered for a cover, because both hold a title and
369
+ a subtitle. `description` is there for a person or a model to choose by.
370
+ - The parts are told from slot names and limits. A title slot called `heading`, or an item with three texts,
371
+ is not recognised; such a slide still gets the alternatives whose slots have the same names.
372
+ - It is not offered by the chat tools yet. Call it from your own code.
107
373
 
108
374
  ## What is not possible
109
375
 
110
376
  - Moving a shape to an arbitrary place from a deck. Positions come only from the template and the manifest.
111
- - Swapping the side of a photo.
377
+ - Mirroring a slide because of a deck's reading direction. A mirrored layout is a separate pattern
378
+ (`derivePattern` with `mirror`) that a deck names or the planner chooses.
379
+ - Deriving a variant that would move a row or grid of items, or a table.
380
+ - Widening a picture inside an item, or making an item taller. A widened item keeps its height, and its
381
+ pictures keep their size.
382
+ - Letting the length of the text decide the size of an item. Only the number of items does.
383
+ - An alternative that needs words the slide does not have. `slideAlternatives` never writes text; ask a model
384
+ for that with `editDeck`.
385
+ - A mirror or text-only variant of a slide with several pictures, or with a picture between two texts.
386
+ - A `box` for a shape inside a group. A variant moves a group as a whole and never resizes what is in it.
112
387
  - A grid with more items than the template slide draws.
113
388
  - New kinds of slides that the template has no sample for.
@@ -66,6 +66,8 @@ operations (`changeManifest`) or by instruction (`editManifest`), and verify it
66
66
  | `patterns` | `PatternDef[]` | yes | The reusable layouts. |
67
67
  | `language` | string | no | BCP 47 tag of the template's sample text. Decks default to it. |
68
68
  | `alignOnDirectionChange` | `"flip"` or `"keep"` | no | What happens to left and right paragraph alignment in a deck that reads the other way. `flip` (default) mirrors it. `keep` leaves it as designed. A text slot can override this. |
69
+ | `textScale` | number | no | Enlarges the body text of every deck made from this template, as a factor on the designed size: `1.3` turns 9 pt into about 12 pt. For a template whose body text is too small to read comfortably. Budgets shrink to match. See "Larger text" in [layout-freedom.md](layout-freedom.md). |
70
+ | `shrinkText` | `{ to, minSize? }` | no | Lets text that does not fit its box be set smaller: down to `to` of its size (`0.85` is 85%) and never under `minSize` points. Acts only for a template loaded with fonts. See "Shrinking text to fit" in [text-fit.md](text-fit.md). |
69
71
 
70
72
  ## `PatternDef`
71
73
 
@@ -91,6 +93,17 @@ operations (`changeManifest`) or by instruction (`editManifest`), and verify it
91
93
 
92
94
  Slots with `empty` of `keep` or `drop` are never offered to the planning model.
93
95
 
96
+ A slot with `"empty": "keep"` and a `box` shows a part of the design in another place: the shape keeps its
97
+ content and is put at the box. Such a slot may point at any shape that is not inside a group (a drawn shape,
98
+ a line, a text, a group as a whole), with `"kind": "text"`, or `"kind": "image"` for a picture. When the box is
99
+ the shape's own size, only its position changes, so nothing inside a group is rescaled. The `derivePattern`
100
+ operation adds these slots, named `moved1`, `moved2`, ..., for shapes that a variant moves and that are not
101
+ slots themselves:
102
+
103
+ ```json
104
+ { "name": "moved1", "kind": "text", "shapeId": "14", "empty": "keep", "box": { "x": 0.52, "y": 0.3, "w": 0.4, "h": 0.05 } }
105
+ ```
106
+
94
107
  ## Text slot (`kind: "text"`)
95
108
 
96
109
  | Field | Type | Meaning |
@@ -150,7 +163,8 @@ The shape must not be inside a group.
150
163
  "min": 2,
151
164
  "max": 4,
152
165
  "span": { "left": 0.04, "right": 0.96 },
153
- "columns": 3
166
+ "columns": 3,
167
+ "widen": 1.5
154
168
  }
155
169
  ```
156
170
 
@@ -162,6 +176,7 @@ The shape must not be inside a group.
162
176
  | `min`, `max` | integer | no | Fewest and most items a deck may ask for. Both default to the number drawn. |
163
177
  | `span` | `{ left, right }` | no | Left and right limits of the row, as fractions of the slide width. Defaults to the row as drawn. Needed when `max` is more than the drawn items can fit in the drawn width. Only applies to shapes placed directly on the slide. |
164
178
  | `columns` | integer | no | For a grid: how many items one line holds on the template slide. Leave it out for a single row. A grid cannot hold more items than it draws. |
179
+ | `widen` | number | no | Lets items grow wider when a line holds fewer of them than the template draws, so that they fill the row and do not stand in the middle of it. The value is the most an item may grow, as a factor on its drawn width: `1.5` lets a card become half as wide again. Backgrounds and text boxes widen; pictures keep their size. The budgets of the row's texts grow to match. Left out, items keep their drawn size. |
165
180
 
166
181
  How the items are laid out for a given count is described in [layout-freedom.md](layout-freedom.md).
167
182
 
@@ -175,11 +190,13 @@ of item 4's slots, named with the next number.
175
190
  - Every `sourceSlide` exists in the pptx.
176
191
  - Every `shapeId` is a shape on that slide, and is the right kind of shape for the slot (a text slot points at
177
192
  a shape with text, an image slot at a picture, a table slot at a table). A slot with `"empty": "drop"` may
178
- point at any shape, because the shape is only ever removed.
193
+ point at any shape, because the shape is only ever removed; so may a slot with `"empty": "keep"` and a `box`,
194
+ because the shape is only ever moved.
179
195
  - Pattern ids are unique. Slot names are unique within a pattern.
180
196
  - A `box` is not set on a shape inside a group. A chart slot's shape is not inside a group.
181
197
  - For a `repeat`: a slot exists for every name template and every drawn item; every shape id exists; all items
182
- sit directly on the slide or all in the same group; `min` is at least 1 and not more than `max`.
198
+ sit directly on the slide or all in the same group; `min` is at least 1 and not more than `max`; `widen`,
199
+ when set, is a number of 1 or more.
183
200
 
184
201
  It also warns about text slots without `maxChars` and about two slots that point at the same shape.
185
202
 
@@ -0,0 +1,217 @@
1
+ # Slide previews
2
+
3
+ A preview is a picture of each slide of a pptx, for showing a deck in an app without opening PowerPoint.
4
+
5
+ slotdeck draws nothing itself. It has a small interface, `PreviewProvider`, that you fill with a renderer of
6
+ your choice, and it calls that interface wherever a picture is wanted: for a deck you rendered, for the
7
+ thumbnails of a template's patterns, and for every presentation the chat tools create.
8
+
9
+ An adapter for one renderer, the Pagus library, comes with the package. Pagus is not a dependency of slotdeck:
10
+ you install it and pass it in.
11
+
12
+ **Read "How close the pictures are" below before relying on a preview.** No renderer that runs without
13
+ PowerPoint was found to reproduce PowerPoint exactly. A preview shows what a deck looks like. To know whether
14
+ a text fits its box, measure it: see [text-fit.md](text-fit.md).
15
+
16
+ ## Setup with Pagus
17
+
18
+ ```
19
+ npm install @pagus-kit/core @pagus-kit/renderer
20
+ ```
21
+
22
+ ```ts
23
+ import { readFileSync, writeFileSync } from 'node:fs';
24
+ import * as pagusCore from '@pagus-kit/core';
25
+ import * as pagusRenderer from '@pagus-kit/renderer';
26
+ import { loadTemplate, pagusPreviews, previewDeck } from 'slotdeck';
27
+
28
+ const preview = pagusPreviews(pagusCore, pagusRenderer);
29
+
30
+ const template = loadTemplate(readFileSync('templates/demo.pptx'), manifest);
31
+ const { pptx, previews } = await previewDeck(template, deck, preview, { assets });
32
+
33
+ for (const picture of previews) {
34
+ writeFileSync(`out/slide-${picture.slide}.svg`, picture.data); // picture.mimeType is 'image/svg+xml'
35
+ }
36
+ ```
37
+
38
+ `previewDeck` is `renderDeck` followed by the provider. To draw a pptx you already have, call the provider
39
+ directly:
40
+
41
+ ```ts
42
+ const previews = await preview.renderSlides({ pptx }); // every slide
43
+ const some = await preview.renderSlides({ pptx, slides: [1, 4] }); // 1-based slide numbers
44
+ ```
45
+
46
+ A 15-slide deck takes about 0.2 seconds.
47
+
48
+ ### What a Pagus preview is
49
+
50
+ - One SVG for each slide, 1280 pixels wide for a 13.33 inch slide (96 pixels to the inch). Shapes, pictures,
51
+ tables and native charts are drawn. Pictures are embedded, so each SVG stands on its own.
52
+ - **The text is real text, set by the browser that shows the SVG.** The browser uses its own fonts and breaks
53
+ the lines itself. The picture therefore needs a browser to be seen, and it looks right only where the
54
+ template's fonts are installed, or where you supply substitutes (see the options).
55
+ - It can be shown with `<img src="...">`, with a `data:` address, or by putting the SVG markup into the page.
56
+ This was checked in Chromium. Do not put the markup of several slides into one page: their internal names
57
+ collide. Use one `<img>` for each.
58
+ - It cannot be turned into a PNG by an SVG library that has no HTML engine (the text is HTML inside the SVG).
59
+ For PNG, take a screenshot of the SVG with a headless browser, or use another provider.
60
+
61
+ ### Options
62
+
63
+ ```ts
64
+ pagusPreviews(pagusCore, pagusRenderer, {
65
+ scale: 0.5, // 640 pixels wide
66
+ fontSubstitutes: { Calibri: 'Carlito', Cambria: 'Caladea' },
67
+ fontCss: "@import url('/fonts/carlito.css');",
68
+ });
69
+ ```
70
+
71
+ | Option | Default | Meaning |
72
+ | --- | --- | --- |
73
+ | `scale` | `1` | Scale of the drawing. `1` gives 96 pixels to the inch. |
74
+ | `fontSubstitutes` | none | For each of the template's typefaces, a typeface to fall back on where the viewer does not have it. |
75
+ | `fontCss` | none | `@font-face` or `@import` rules to embed in each SVG, so that it brings its own fonts. A browser does not fetch fonts by address for an SVG shown through `<img>`: embed them as data in the rule, or put the markup into the page. |
76
+
77
+ ### Right-to-left text
78
+
79
+ Pagus leaves the direction of a paragraph to the browser, which takes a paragraph to run left to right unless
80
+ told otherwise. The adapter therefore marks every paragraph that starts with a right-to-left letter as running
81
+ right to left, the same rule slotdeck's renderer uses when it writes the pptx. Without this, a Hebrew sentence
82
+ gets its full stop on the wrong side and a line that mixes Hebrew with an address or a number comes out in the
83
+ wrong order.
84
+
85
+ ## Thumbnails of a template's patterns
86
+
87
+ ```ts
88
+ import { patternPreviews, previewExtension } from 'slotdeck';
89
+
90
+ const pictures = await patternPreviews(template, preview);
91
+ // { cover: SlidePreview, 'text-and-photo': SlidePreview, ... } one for each pattern id
92
+
93
+ for (const [id, picture] of Object.entries(pictures)) {
94
+ writeFileSync(`thumbnails/${id}.${previewExtension(picture)}`, picture.data);
95
+ }
96
+ ```
97
+
98
+ Each picture is the pattern's slide of the template's sample deck (`sampleDeck`), so a pattern derived from
99
+ another pattern's template slide is shown as it will look, with sample words in its slots. The options of
100
+ `sampleDeck` (`fill`, `items`, `fit`) can be passed as a third argument. Make the thumbnails once, when a
101
+ template or its manifest changes, and serve them as files: they are what the `thumbnail` option of the chat
102
+ tools points at (see [chat-tools.md](chat-tools.md)).
103
+
104
+ The same call gives the studio a picture of a manifest's sample deck, to judge budgets by eye:
105
+
106
+ ```ts
107
+ const { deck, assets } = sampleDeck(template, { items: 'max' });
108
+ const { previews } = await previewDeck(template, deck, preview, { assets, layout: 'adaptive' });
109
+ ```
110
+
111
+ ## Previews from the chat tools
112
+
113
+ Give `presentationTools` a provider, and every presentation that `create_presentation` or `edit_presentation`
114
+ makes is drawn:
115
+
116
+ ```ts
117
+ const tools = presentationTools({ template, llm, store, preview });
118
+ ```
119
+
120
+ - The pictures are kept in the store under `preview/<presentation id>/slide-01.svg`, `slide-02.svg`, and so on
121
+ (`storeKeys.preview(id, slide, extension)`; the extension follows the provider's picture type).
122
+ - The tool's `output` for the chat model lists the keys in `previews`. The pictures themselves are not given to
123
+ the model.
124
+ - The `ToolResult` for your app carries them in `previews`, next to `deck` and `pptx`.
125
+ - If the provider fails, the presentation is still created and stored. `output.preview_error` then holds the
126
+ message, and there is no `previews`.
127
+
128
+ ## Writing your own provider
129
+
130
+ ```ts
131
+ import type { PreviewProvider } from 'slotdeck';
132
+
133
+ const preview: PreviewProvider = {
134
+ async renderSlides({ pptx, slides, signal }) {
135
+ // Turn the pptx into pictures in any way: a service that runs PowerPoint or LibreOffice,
136
+ // a headless browser, another library.
137
+ const pngs: Uint8Array[] = await myRenderer.toPng(pptx, { signal });
138
+ return pngs
139
+ .map((data, index) => ({ slide: index + 1, mimeType: 'image/png', data }))
140
+ .filter((picture) => !slides || slides.includes(picture.slide));
141
+ },
142
+ };
143
+ ```
144
+
145
+ The contract:
146
+
147
+ | Field | Meaning |
148
+ | --- | --- |
149
+ | `request.pptx` | The presentation's bytes. |
150
+ | `request.slides` | 1-based numbers of the slides wanted; every slide when left out. |
151
+ | `request.signal` | Stop when it is aborted. |
152
+ | result | One `SlidePreview` for each slide asked for, in slide order: `{ slide, mimeType, data, width?, height? }`. `mimeType` is `image/svg+xml` or a bitmap type such as `image/png`; `data` is the SVG as UTF-8 text or the bitmap's bytes. |
153
+
154
+ A service that runs PowerPoint itself, or LibreOffice, is the way to pictures that match what people will see
155
+ in PowerPoint, and to PNG. A PNG preview also fits `draftManifest`'s `slideImage` option, which takes the same
156
+ `{ mimeType, data }` shape.
157
+
158
+ ## How close the pictures are
159
+
160
+ Three renderers were tried on this project's demo template and its two example decks (`examples/decks`, in
161
+ English and in Hebrew: 22 slides with text, cards, a table, picture charts and a native chart), and compared
162
+ with PowerPoint 16's own export of the same files. For the two that produce SVG with real text, the SVG was
163
+ drawn by Chromium on a machine that has the template's fonts.
164
+
165
+ | Renderer | Needs a browser | English | Hebrew (right to left) | Tables | Native charts |
166
+ | --- | --- | --- | --- | --- | --- |
167
+ | Pagus 0.1.1 (`@pagus-kit/renderer`), through the adapter | yes | Close: same typefaces and weights, same line breaks in running text | Readable, in the right order; punctuation and mixed lines right with the adapter's marking | Styling close; a right-to-left table is drawn left to right | Close: bars, values, legend, gridlines; the axis steps differ |
168
+ | pptx-svg 0.6.8 | yes | Positions right; a title fell back to another typeface; line breaks depend on a width function you supply | Readable; full stops on the wrong side and mixed lines in the wrong order | Grid lines drawn that the template does not have; not mirrored | Drawn with another layout (legend at the top right, a frame) |
169
+ | pptx-glimpse 5.3.1 | no (draws text as outlines, makes PNG) | Shapes and pictures right; bold and italic are lost, so lines break elsewhere | **Unreadable: the letters of every word come out in reverse order** | Styling lost; not mirrored | Bars, axis and legend; no values on the bars, no gridlines |
170
+
171
+ Measured as the mean difference between the two pictures of a slide (greyscale, 760 pixels wide), Pagus was
172
+ closest: 1.2% for the English deck and 1.3% for the Hebrew one, against 1.6% and 1.8% for pptx-glimpse and
173
+ 2.0% and 1.7% for pptx-svg. The figure says little about text: the Hebrew slides of pptx-glimpse score well
174
+ and cannot be read.
175
+
176
+ What is wrong in a Pagus preview, as seen on these decks:
177
+
178
+ - **Text anchored to the bottom of its box is drawn from the top.** A cover title that sits low on the slide
179
+ in PowerPoint sits higher in the preview. Text anchored to the middle is placed right.
180
+ - **A line that just fits in PowerPoint may wrap in the browser**, and the reverse. The browser measures text
181
+ a little differently. In running text the breaks were the same on the slides checked; a title that fills
182
+ its line to the last point wrapped.
183
+ - **A right-to-left table is not mirrored:** its first column is on the left.
184
+ - **Rounded corners of a picture are not drawn**, and a bullet sits closer to its text.
185
+ - **A native chart's axis** has other steps, and the bars stand a little differently.
186
+ - **Fonts.** The viewer's browser needs the template's fonts. Without them it falls back, and every line can
187
+ break elsewhere.
188
+
189
+ Other findings worth knowing:
190
+
191
+ - pptx-glimpse 5.3.1 fails outright when it is given Calibri or Cambria as font files
192
+ (`lookupType: 7 - substFormat: 1 is not yet supported`). Those are the fonts it finds first on a Windows
193
+ machine, so there it fails with its default settings. It runs with open fonts such as Carlito and Caladea,
194
+ and then uses one face for each family: the bold and italic files are not told apart.
195
+ - pptx-glimpse draws a shadow under shapes for which the template turns shadows off.
196
+ - pptx-svg in Node needs a function that gives the width of a text. It is told the typeface and the size, not
197
+ the weight. Given real widths from the font files (see [text-fit.md](text-fit.md)), its lines for regular
198
+ text were the ones PowerPoint made.
199
+ - pptx-browser 4.1.5 draws on a canvas. In Node that needs the `canvas` package, which is native code, so it
200
+ was not tried.
201
+
202
+ In short: Pagus in a browser is good enough to show a person what a deck looks like, in English and in Hebrew.
203
+ None of the three can stand in for PowerPoint when the question is whether a slide is right to the line. That
204
+ question is answered by measurement ([text-fit.md](text-fit.md)), which agrees with PowerPoint line for line.
205
+
206
+ ### Checking a renderer yourself
207
+
208
+ The repository has what the comparison was made with. It needs Windows with PowerPoint and is not part of the
209
+ package.
210
+
211
+ ```
212
+ node examples/render-demo.ts # writes out/patterns-en.pptx and out/patterns-he.pptx
213
+ powershell -File scripts/render-pptx.ps1 -Path out/patterns-he.pptx -OutDir out/ppt-he -Width 1280
214
+ ```
215
+
216
+ `render-pptx.ps1` exports PowerPoint's own picture of every slide. Draw the same file with your provider and
217
+ put the two side by side.