Skip to content

Commit 652f804

Browse files
feat(pptx/render): re-land #96 — applyEdits layout-instantiation + headless render API + font transparency (#98)
* feat(pptx): layout-instantiation in applyEdits (lossless scale-with-variety) Support source:{layoutId,fills} in a PlannedSlide — instantiate a fresh slide from one of the template's OWN layouts inside the lossless byte-patch path. The layout is already a part of source, so the new slide binds to its ppt/slideLayouts/<id>.xml (inheriting theme/master/background chrome) while every other part stays byte-identical. - Each layout placeholder becomes an addressable, positioned element keyed by the deterministic id layoutSlotElementId(layoutId, key) (exported). Text/obj slots fill from fills + edit via setText; picture slots get a transparent placeholder blip so setImage can repoint them; chart/table/other expose geometry for addChart/addDiagram. - Placeholder geometry is read EMU-native from the layout, falling back to the matching master slot — no canvas-px round-trip. - Unresolvable layoutId → onWarning + skip (never ship a wrong slide). Threads an instantiated-block map through the edit ops so edits resolve against the freshly-built slide XML. Tests: mixed clone+instantiate (3 slides, correct layout rel, chrome inherited, cloned parts byte-identical, zero dangling rels), master-geometry inheritance, unresolved-layout skip. * feat(render): headless renderDeckToImages + deck.fontUsage font transparency Headless render-to-image for a server-side visual-QA loop. Browser-free (no Playwright/Chromium/DOM): composes a deterministic SVG per slide that draws what the editor draws — native charts (buildChartOption + ECharts SSR), diagrams (layoutDiagram), text/shapes/images/backgrounds in z-order — not the OOXML raster fallbacks. - renderDeckToSvg / renderDeckToImages / renderSlideToImage / renderPptxToImages. - Rasterisation is an injected hook (opts.rasterizeSvg, e.g. @resvg/resvg-js); default tries a dynamic resvg import and throws a clear error otherwise — no hard native dep. ECharts is loaded on demand so it never bloats the editor bundle (main chunk stays ~1.1MB). - opts: slides (1-based subset), dpi, format, maxWidth. Font transparency: parsePptx stamps deck.fontUsage {family,embedded}[] — every family the text uses, flagged embedded (a real ppt/fonts/* part in <p:embeddedFontLst>) vs only referenced, so the host can warn on missing brand fonts. Read-only diagnostic, distinct from deck.fonts. Tests: render-deck (chart colours, diagram, image, subset, injected rasteriser, maxWidth, browser-free) + font-usage (embed vs reference).
1 parent 9142445 commit 652f804

12 files changed

Lines changed: 1613 additions & 33 deletions

File tree

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
"@textcortex/slidewise": minor
3+
---
4+
5+
feat(pptx): layout-instantiation in `applyEdits` (lossless scale-with-variety)
6+
7+
`applyEdits` now supports `source: { layoutId, fills? }` in a `PlannedSlide`
8+
instantiating a fresh slide from one of the template's **own** layouts inside
9+
the lossless byte-patch path. Because the layout is already a part of `source`,
10+
the new slide binds to `ppt/slideLayouts/<layoutId>.xml` (inheriting theme /
11+
master / background chrome) while every other part stays byte-identical. This
12+
unlocks lossless **and** scale-with-variety in one deck: clone slides where you
13+
want the exact thing, instantiate from layouts where you want variety.
14+
15+
Each layout placeholder is materialised as an addressable, positioned element
16+
with a deterministic id — `layoutSlotElementId(layoutId, key)` (exported) where
17+
`key` is the `placeholderKey` / `summarizeLayouts` slot key. Text/`obj` slots
18+
are populated from `fills` and editable via `setText`; picture slots become a
19+
`<p:pic>` with a transparent placeholder blip so `setImage` can repoint them;
20+
chart/table/other slots expose their geometry so the host fills them with
21+
`addChart` / `addDiagram`. Placeholder geometry is read EMU-native from the
22+
layout (falling back to the matching master slot), so it stays correct without a
23+
canvas-px round-trip. An unresolvable `layoutId` is surfaced via `onWarning` and
24+
the slide is skipped rather than shipped wrong.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
"@textcortex/slidewise": minor
3+
---
4+
5+
feat(render): headless `renderDeckToImages` + `deck.fontUsage` font transparency
6+
7+
**Headless render-to-image (visual-QA loop).** New browser-free renderer that
8+
draws **what the editor draws** — native charts (`buildChartOption` + ECharts
9+
SSR), diagrams (`layoutDiagram`), text/shapes/images/backgrounds in z-order —
10+
*not* the OOXML raster fallbacks. No Playwright/Chromium/DOM.
11+
12+
- `renderDeckToSvg(deck, opts?)` → one composed SVG per slide (ECharts is
13+
loaded on demand, so it never bloats the editor bundle).
14+
- `renderDeckToImages(deck, opts?)` / `renderSlideToImage(deck, i, opts?)` /
15+
`renderPptxToImages(bytes, opts?)` → raster bytes. Rasterisation is an
16+
injected hook (`opts.rasterizeSvg`, e.g. `@resvg/resvg-js`); when omitted the
17+
default tries a dynamic `@resvg/resvg-js` import and throws a clear error if
18+
it isn't installed — so there's no hard native dependency.
19+
- `opts`: `slides` (1-based subset), `dpi` (canvas scales by `dpi/96`),
20+
`format`, `maxWidth` (thumbnail cap). Deterministic (no animation).
21+
22+
Enables the host's render → fresh-eyes inspect → targeted `applyEdits` fix →
23+
re-render cycle, rendering a final `applyEdits` output directly.
24+
25+
**Font transparency.** `parsePptx` now stamps `deck.fontUsage:
26+
{ family, embedded }[]` — every font family the deck's text uses, flagged
27+
whether the source PPTX actually **embeds** it (`<p:embeddedFontLst>` → a real
28+
`ppt/fonts/*` part) or merely **references** it (system-fallback risk on
29+
viewers that don't ship the brand font). Hosts use it to warn at generation
30+
time ("missing fonts for some ppts"). It's a read-only diagnostic, distinct from
31+
`deck.fonts` (the embeddable payloads).

README.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,97 @@ automatically. `serializeDeck` remains the path for the live editor and
165165
from-scratch decks; `applyEdits` is the lossless path for template-derived
166166
output.
167167

168+
**Scaling a deck with the template's own layouts.** A `PlannedSlide` can clone
169+
a source slide (`{ slideIndex }`) **or** instantiate a fresh slide from one of
170+
the template's layouts (`{ layoutId, fills? }`). Because the layout is already a
171+
part of `source`, instantiation is still a lossless patch — the new slide binds
172+
to `ppt/slideLayouts/<layoutId>.xml` and inherits its theme / master /
173+
background chrome, while every other part stays byte-identical. This is how you
174+
build a 35-slide deck from a 16-slide template without it looking repetitive:
175+
clone where you want the exact slide, instantiate from layouts where you want
176+
variety.
177+
178+
```ts
179+
import { applyEdits, layoutSlotElementId, summarizeLayouts } from "@textcortex/slidewise";
180+
181+
const layouts = summarizeLayouts(deck); // pick a layout id + its fillable slot keys
182+
const layoutId = layouts[0].id;
183+
184+
await applyEdits(source, {
185+
slides: [
186+
{ source: { slideIndex: 1 }, edits: [] }, // cloned, byte-identical
187+
{
188+
// Instantiate from a layout; `fills` populates text placeholders by key.
189+
source: { layoutId, fills: { title: "Pipeline", "body:1": "Q3 → Q4" } },
190+
edits: [
191+
// Non-text slots are addressable by a deterministic id so edits can
192+
// target them: fill the picture slot, draw a chart into the chart slot.
193+
{ op: "setImage", elementId: layoutSlotElementId(layoutId, "pic:2"), data: photoBytes },
194+
{ op: "addChart", bounds: chartSlotBounds, kind: "column", categories, series },
195+
],
196+
},
197+
],
198+
});
199+
```
200+
201+
Each instantiated placeholder (text **and** non-text — picture / chart / table)
202+
is materialised as a positioned element with the stable id
203+
`layoutSlotElementId(layoutId, key)`, where `key` is the `placeholderKey` /
204+
`summarizeLayouts` slot key (`"title"`, `"body:1"`, `"pic:2"`, …). An
205+
unresolvable `layoutId` is reported via `onWarning` and the slide is skipped
206+
(never shipped wrong).
207+
208+
### Headless render-to-image (visual QA)
209+
210+
`renderDeckToImages` renders a deck to one image per slide **server-side, with
211+
no browser** — drawing what the editor draws (native charts via ECharts SSR,
212+
diagrams, text, shapes, images), not the OOXML raster fallbacks. It's built for
213+
a render → inspect → fix → re-render QA loop, e.g. rendering a final `applyEdits`
214+
output and having a model flag overflow / overlap / leftover text.
215+
216+
```ts
217+
import {
218+
renderDeckToSvg,
219+
renderDeckToImages,
220+
renderPptxToImages,
221+
} from "@textcortex/slidewise";
222+
223+
// SVGs only (no rasteriser needed) — rasterise yourself if you prefer.
224+
const svgs: string[] = await renderDeckToSvg(deck, { slides: [1, 2] });
225+
226+
// Raster bytes. Rasterisation is an injected hook so there's no hard native dep
227+
// — pass a @resvg/resvg-js wrapper (the default tries to import it on demand).
228+
import { Resvg } from "@resvg/resvg-js";
229+
const pngs: Uint8Array[] = await renderDeckToImages(deck, {
230+
dpi: 150,
231+
rasterizeSvg: (svg, width) => new Resvg(svg, { fitTo: { mode: "width", value: width } }).render().asPng(),
232+
});
233+
234+
// Render a final applyEdits output directly:
235+
const shots = await renderPptxToImages(await applyEdits(source, plan));
236+
```
237+
238+
`opts`: `slides` (1-based subset), `dpi` (the 1920×1080 canvas scales by
239+
`dpi/96`), `format`, `maxWidth` (thumbnail cap). Output is deterministic (no
240+
animation). The renderer is browser-free and ECharts is loaded on demand, so it
241+
never bloats the editor bundle.
242+
243+
### Font transparency (missing-font warnings)
244+
245+
`parsePptx` stamps `deck.fontUsage: { family, embedded }[]` — every font family
246+
the deck's text uses, flagged whether the source PPTX actually **embeds** it
247+
(`<p:embeddedFontLst>` → a real `ppt/fonts/*` part) or only **references** it (so
248+
it falls back to a system font on viewers that don't ship the brand font). Use it
249+
to warn at generation time:
250+
251+
```ts
252+
const missing = (deck.fontUsage ?? []).filter((f) => !f.embedded);
253+
if (missing.length) warnHost(`not embedded: ${missing.map((f) => f.family).join(", ")}`);
254+
```
255+
256+
This is a read-only diagnostic, distinct from `deck.fonts` (the embeddable
257+
payloads the serializer writes back).
258+
168259
### Generating slides from the template's layouts
169260

170261
`parsePptx` exposes the source template's master layouts on `deck.layouts`.

packages/slidewise/src/index.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,13 @@ export {
8888
type SlideRailItemContextValue,
8989
} from "./compound";
9090

91-
export { parsePptx, isPptxTemplate, serializeDeck, applyEdits } from "./lib/pptx";
91+
export {
92+
parsePptx,
93+
isPptxTemplate,
94+
serializeDeck,
95+
applyEdits,
96+
layoutSlotElementId,
97+
} from "./lib/pptx";
9298
export type {
9399
SerializeOptions,
94100
SerializeWarning,
@@ -148,6 +154,21 @@ export {
148154
type DiagramArrowPrimitive,
149155
} from "./lib/diagram/layout";
150156

157+
/**
158+
* Headless deck → image rendering for a server-side visual-QA loop. Browser-free
159+
* (no Playwright/DOM): composes a deterministic SVG per slide that draws what
160+
* the editor draws — native charts, diagrams, text, shapes, images — then
161+
* rasterises via an injected `rasterizeSvg` hook (e.g. `@resvg/resvg-js`).
162+
* `renderDeckToSvg` returns the SVGs directly if you'd rather rasterise yourself.
163+
*/
164+
export {
165+
renderDeckToSvg,
166+
renderDeckToImages,
167+
renderSlideToImage,
168+
renderPptxToImages,
169+
type RenderOptions,
170+
} from "./lib/render/renderDeck";
171+
151172
export type {
152173
Deck,
153174
Slide,
@@ -184,6 +205,7 @@ export type {
184205
GlowSpec,
185206
DashType,
186207
FontAsset,
208+
FontUsage,
187209
WebFontAsset,
188210
} from "./lib/types";
189211
export { SLIDE_W, SLIDE_H } from "./lib/types";

0 commit comments

Comments
 (0)