返回 slidev
ir.ts
1 /**
2 * Intermediate representation between the rendered DOM and a `.pptx`.
3 * `RawSnapshot` is what the in-page walker returns and stays close to the DOM
4 * (code inside `page.evaluate` is unreachable from unit tests). `SlideIr` is
5 * what the normalizer produces and stays close to what `pptxgenjs` wants.
6 * Every decision lives in `normalize.ts` between the two, as pure functions
7 * over plain JSON, testable without a browser.
8 */
9
10 /**
11 * All geometry is in canvas pixels: `canvasWidth` by `round(canvasWidth / aspectRatio)`,
12 * origin at the top-left of the slide (not of the print page). `exportSlides`
13 * sizes the PowerPoint slide as `width / 96` by `height / 96` inches, so one
14 * CSS pixel is exactly 1/96 inch. 1px = 1pt does not hold in general.
15 */
16 export const EMU_PER_PX = 9525 // 914400 EMU per inch / 96 px per inch
17 export const INCHES_PER_PX = 1 / 96
18
19 /** Points per pixel (72 pt per inch against 96 px per inch), for font size, letter spacing and line height. */
20 export const PT_PER_PX = 0.75 // 72 / 96
21
22 export interface Rect {
23 x: number
24 y: number
25 w: number
26 h: number
27 }
28
29 /**
30 * The computed-style subset the walker captures, as raw CSS strings; parsing
31 * happens on the Node side. Interned: `RawNode.style` indexes `RawSnapshot.styles`.
32 */
33 export interface RawStyle {
34 display: string
35 position: string
36 zIndex: string
37 visibility: string
38 opacity: string
39 color: string
40 backgroundColor: string
41 backgroundImage: string
42 fontFamily: string
43 fontSize: string
44 fontWeight: string
45 fontStyle: string
46 textAlign: string
47 textDecorationLine: string
48 textTransform: string
49 letterSpacing: string
50 lineHeight: string
51 whiteSpace: string
52 paddingLeft: string
53 paddingRight: string
54 borderTopWidth: string
55 borderTopStyle: string
56 borderTopColor: string
57 borderRightWidth: string
58 borderRightStyle: string
59 borderRightColor: string
60 borderBottomWidth: string
61 borderBottomStyle: string
62 borderBottomColor: string
63 borderLeftWidth: string
64 borderLeftStyle: string
65 borderLeftColor: string
66 borderTopLeftRadius: string
67 boxShadow: string
68 filter: string
69 backdropFilter: string
70 mixBlendMode: string
71 clipPath: string
72 transform: string
73 writingMode: string
74 webkitBackgroundClip: string
75 overflow: string
76 top: string
77 right: string
78 bottom: string
79 left: string
80 width: string
81 height: string
82 }
83
84 export interface RawNode {
85 /** Index into `RawSnapshot.nodes`, and the value of `data-slidev-export-id`. */
86 id: number
87 /** Parent's `id`, or -1 for the slide container itself. */
88 parent: number
89 /** Upper-case tag name, or `#text` for a text node. */
90 tag: string
91 /** Index into `RawSnapshot.styles`; -1 for text nodes, which have no style. */
92 style: number
93 /** Relative to the slide container, not to the print page. */
94 rect: Rect
95 /** Text nodes only: `Range.getClientRects()`, slide-relative. Kept as the raw list so the normalizer can count line boxes; a text box positioned from the element rect lands offset and re-wraps. */
96 glyphRects?: Rect[]
97 /** Text nodes only: raw `textContent`, before `text-transform` is applied. */
98 text?: string
99 /** One rect per line box for a wrapped inline element. Backgrounds and borders paint per line fragment, not over the `getBoundingClientRect()` union. */
100 fragments?: Rect[]
101 /** `IMG` only. */
102 src?: string
103 /** `IMG` only. */
104 alt?: string
105 /** `A` only, resolved absolute. */
106 href?: string
107 /** Root of a rendered formula. KaTeX sets it as dozens of positioned spans in its own metric fonts; walked as text the layout falls apart, so it is rasterized whole. */
108 isMath?: boolean
109 /** An `SVG` carrying `<foreignObject>` (as Mermaid renders), so its labels are HTML. No PowerPoint renderer draws that; it has to be rasterized. */
110 hasForeignObject?: boolean
111 /** Reached through `el.shadowRoot`. Mermaid diagrams live in one. */
112 fromShadowRoot?: boolean
113 /** Opacity compounded from every ancestor, present only when below 1. DrawingML has no group opacity, so a wrapper's transparency is folded into each descendant's colors. */
114 opacity?: number
115 /** A `::marker` list bullet, which is a pseudo-element and has no text node. */
116 marker?: string
117 /** Page coordinates. A `::before` or `::after` has no element of its own, so a screenshot clips the page instead of targeting a locator. */
118 pageRect?: Rect
119 }
120
121 export interface RawSlide {
122 /** 1-based deck slide number, parsed from the container id `003-02`. */
123 no: number
124 /** 0-based click step within that slide. */
125 clickIndex: number
126 /** The container's exact DOM id, e.g. `003-02`. A selector rebuilt from `no` matches every click step of the slide, so screenshots target this instead. */
127 containerId: string
128 size: { w: number, h: number }
129 /** The slide's own background color, as a raw CSS string. */
130 background?: string
131 nodes: RawNode[]
132 }
133
134 export interface RawSnapshot {
135 slides: RawSlide[]
136 styles: RawStyle[]
137 /**
138 * CSS font stack as written, mapped to the family the browser actually resolved,
139 * measured by comparing rendered widths in the page. `document.fonts.check()`
140 * cannot be used: it returns true for families that do not exist.
141 */
142 fontResolution: Record<string, string>
143 /** Pseudo-elements that paint something but take part in flow, so their box cannot be resolved from computed style. Reported so the loss is logged rather than silent. */
144 unplaceablePseudos: string[]
145 }
146
147 /** Straight (non-premultiplied) alpha, 0 to 1. */
148 export interface Rgba {
149 r: number
150 g: number
151 b: number
152 a: number
153 }
154
155 export interface Border {
156 width: number
157 color: Rgba
158 style: 'solid' | 'dashed' | 'dotted'
159 }
160
161 interface IrBase {
162 rect: Rect
163 /** The `RawNode.id` this came from. Only for diagnostics. */
164 sourceId: number
165 }
166
167 export interface IrBox extends IrBase {
168 kind: 'box'
169 fill?: Rgba
170 /**
171 * Top, right, bottom, left; a side is undefined when it has no visible border.
172 * `pptxgenjs` gives a shape a single uniform `line`, so the builder emits
173 * each differing side as its own filled rectangle.
174 */
175 borders?: [Border?, Border?, Border?, Border?]
176 /** Pixels. The builder converts; `rectRadius` is in inches despite its docs. */
177 radius?: number
178 shadow?: { blur: number, offset: number, angle: number, color: Rgba }
179 }
180
181 export interface IrRun {
182 /** Already `text-transform`ed. */
183 text: string
184 /** Pixels. */
185 fontSize: number
186 /** Already resolved to a family that exists on this machine. */
187 fontFamily: string
188 bold?: boolean
189 italic?: boolean
190 underline?: boolean
191 /**
192 * How the underline is drawn. A `border-bottom` on inline text is an
193 * underline, and DrawingML can dash one, so it does not need a shape.
194 */
195 underlineStyle?: 'sng' | 'dash' | 'dotted'
196 strike?: boolean
197 color?: Rgba
198 /** Pixels. */
199 letterSpacing?: number
200 link?: string
201 /** Emit a line break before this run. From `<br>`. */
202 breakBefore?: boolean
203 /** This run ends a paragraph. */
204 endsParagraph?: boolean
205 }
206
207 export interface IrText extends IrBase {
208 /** `rect` is glyph bounds, not the element box. See `RawNode.glyphRects`. */
209 kind: 'text'
210 elementRect: Rect
211 /** Distinct line-box tops. One means the box must not be allowed to re-wrap. */
212 lineCount: number
213 align: 'left' | 'center' | 'right' | 'justify'
214 /** Defaults to top, since the box is measured from the glyphs; middle is for chip labels, whose box is the decoration rather than the ink. */
215 valign?: 'top' | 'middle'
216 /** Pixels, with `normal` already resolved to a number. */
217 lineHeight: number
218 runs: IrRun[]
219 }
220
221 export interface IrImage extends IrBase {
222 kind: 'image'
223 /** A `data:` URI. Always raster; SVG is routed to `IrRaster` instead. */
224 data: string
225 alt?: string
226 link?: string
227 /**
228 * The visible region when the slide edge cuts the image short: draw at full
229 * size, then crop to `rect`, as the browser does; squeezing into the clipped
230 * box would compress it. All values are CSS pixels: `w`/`h` the full display
231 * size, `x`/`y` the offset from its top-left corner to `rect`.
232 */
233 crop?: { x: number, y: number, w: number, h: number }
234 }
235
236 export type RasterReason
237 = | 'svg'
238 | 'math'
239 | 'foreign-object'
240 | 'canvas'
241 | 'media'
242 | 'iframe'
243 | 'background-image'
244 | 'background-clip-text'
245 | 'filter'
246 | 'backdrop-filter'
247 | 'mix-blend-mode'
248 | 'clip-path'
249 | 'transform'
250 | 'writing-mode'
251
252 export interface IrRaster extends IrBase {
253 kind: 'raster'
254 /** A PNG `data:` URI, filled in by the capture phase. */
255 data: string
256 reason: RasterReason
257 /** Capture with everything else on the page hidden. `locator.screenshot()` clips the page to the element's box rather than isolating the element, so overlapping content would land in the picture and be drawn twice. */
258 isolate: boolean
259 /** Also hide the element's own children while capturing. Correct only when they are walked and redrawn as shapes; hiding a leaf's children (e.g. an `<svg>`) removes its artwork from the picture. */
260 hideDescendants: boolean
261 }
262
263 export type IrNode = IrBox | IrText | IrImage | IrRaster
264
265 export interface SlideIr {
266 /** 1-based deck slide number. */
267 no: number
268 clickIndex: number
269 /** The rendered container's exact DOM id, for targeting a screenshot. */
270 containerId: string
271 size: { w: number, h: number }
272 background?: Rgba
273 /** Paint order is array order: an inline decoration must be emitted before the text it sits behind. */
274 nodes: IrNode[]
275 note?: string
276 /**
277 * When set, `nodes` is ignored and the slide is written as a single background
278 * picture, exactly as `--format pptx` does. Set when the walk failed, the
279 * slide lost all of its text, or so much rasterized that vectorizing bought nothing.
280 */
281 fallbackPng?: string
282 fallbackReason?: string
283 }
284
284 lines TYPESCRIPT