@bendyline/squisq-cli 2.5.4 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -1
- package/THIRD_PARTY_LICENSES.txt +2 -2
- package/dist/{api-E5PDCULT.js → api-O3IMGDPH.js} +13 -3
- package/dist/api.d.ts +65 -3
- package/dist/api.js +192 -71
- package/dist/{chunk-SFBZGL54.js → chunk-2ZJW7F62.js} +193 -72
- package/dist/index.js +172 -15
- package/dist/squisq-player.full.global.js +434 -432
- package/dist/squisq-player.global.js +63 -61
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -96,9 +96,45 @@ Notes:
|
|
|
96
96
|
- **An existing output file is never overwritten by default.** The check runs before rendering, so a colliding path costs you a second rather than a full capture. Pass `--overwrite` to replace.
|
|
97
97
|
- FFmpeg failures report the one relevant line (e.g. `[libx264] width not divisible by 2 (851x480) (exit code 1)`) rather than dumping the whole command line and stderr buffer.
|
|
98
98
|
|
|
99
|
+
### `squisq image <input> [output]`
|
|
100
|
+
|
|
101
|
+
Render a document's **Dashboard** rendition — every block arranged on one
|
|
102
|
+
canvas — to a PNG image. Needs Playwright Chromium only (no ffmpeg; this is a
|
|
103
|
+
single-frame capture, not a video encode). The layout comes from the doc's
|
|
104
|
+
`squisq-dashboard-layout` frontmatter (or `--layout`), defaulting to the
|
|
105
|
+
auto-pick that chooses the smallest built-in layout fitting the block count;
|
|
106
|
+
the document-title band renders by default and `--no-title` hides it. `--style`
|
|
107
|
+
picks the cell dressing (`basic`, `card`, `panel`, `accent` — all theme-derived),
|
|
108
|
+
defaulting to the doc's `squisq-dashboard-style`.
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
squisq image input.md output.png
|
|
112
|
+
squisq image input.md --resolution 4k
|
|
113
|
+
squisq image input.md --resolution square --layout grid-2x2
|
|
114
|
+
squisq image input.md --style card
|
|
115
|
+
squisq image input.md --width 1280 --height 720 --no-title
|
|
116
|
+
squisq image input.md -t documentary --transform magazine
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
| Option | Description | Default |
|
|
120
|
+
| ------------------------ | ---------------------------------------------------------------------------------------------------------- | ------------- |
|
|
121
|
+
| `-o, --output` | Output `.png` path | `<input>.png` |
|
|
122
|
+
| `--resolution` | Named preset: `hd`, `fhd`, `4k`, `square`, `square-2k`, `portrait`, `portrait-4k`, `standard` | `fhd` |
|
|
123
|
+
| `--width` / `--height` | Custom pixels (both required together; excludes `--resolution`). 64–7680 per axis, ≤33 MP; odd values fine | preset |
|
|
124
|
+
| `--layout` | Dashboard layout id (built-ins or a doc-defined custom layout), or `auto` | `auto` |
|
|
125
|
+
| `--style` | Cell style variant: `basic`, `card`, `panel`, `accent` (all theme-derived) | doc setting |
|
|
126
|
+
| `--title` / `--no-title` | Show/hide the document-title band | shown |
|
|
127
|
+
| `--format` | Output format (`png`) | `png` |
|
|
128
|
+
| `-t, --theme` | Squisq theme id to apply | none |
|
|
129
|
+
| `--transform` | Transform style to apply before rendering | none |
|
|
130
|
+
| `--overwrite` | Replace an existing output file (otherwise refuse and exit non-zero) | off |
|
|
131
|
+
| `--no-auto-templates` | Disable content-aware template auto-picking for unannotated headings | (auto on) |
|
|
132
|
+
|
|
133
|
+
The same export is available through the registry as `squisq convert <input> -f png`.
|
|
134
|
+
|
|
99
135
|
### `squisq doctor`
|
|
100
136
|
|
|
101
|
-
Preflight the
|
|
137
|
+
Preflight the rendering toolchain: reports the resolved ffmpeg path, version, and which source it came from (`SQUISQ_FFMPEG` env / `PATH` / `ffmpeg-static`) with an install hint when missing, attempts a headless Chromium launch, reports the Node version, and summarizes per-feature readiness (video/GIF need ffmpeg + Chromium; the dashboard PNG image needs Chromium only).
|
|
102
138
|
|
|
103
139
|
```bash
|
|
104
140
|
squisq doctor
|
|
@@ -222,6 +258,27 @@ captions, an infinite loop, and disabled slide animations/transitions. Set
|
|
|
222
258
|
`captionStyle: 'off'` to omit captions. Embedded video still advances, but all
|
|
223
259
|
audio is omitted and reported through `result.warnings`.
|
|
224
260
|
|
|
261
|
+
### `renderDocToDashboardPng`
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
import { renderDocToDashboardPng } from '@bendyline/squisq-cli/api';
|
|
265
|
+
|
|
266
|
+
const result = await renderDocToDashboardPng(doc, input.container, {
|
|
267
|
+
outputPath: './dashboard.png',
|
|
268
|
+
resolution: 'square', // or width/height for custom pixels
|
|
269
|
+
layout: 'grid-2x2', // 'auto' (default) picks by block count
|
|
270
|
+
style: 'card', // 'basic' (default) | 'card' | 'panel' | 'accent'
|
|
271
|
+
title: true,
|
|
272
|
+
});
|
|
273
|
+
// result: { bytes: Uint8Array, width, height, outputPath? }
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Renders the Dashboard rendition to a single PNG. Needs Playwright Chromium
|
|
277
|
+
only — no ffmpeg. `resolution` names a preset from `DASHBOARD_RESOLUTIONS`
|
|
278
|
+
(default `fhd`, 1920×1080); explicit `width`/`height` (both together) replace
|
|
279
|
+
it. Apply a theme upstream (`{ ...doc, themeId }`), the same convention the
|
|
280
|
+
video renderers use.
|
|
281
|
+
|
|
225
282
|
### Native frame encoding
|
|
226
283
|
|
|
227
284
|
For callers that already have PNG frames and do not need Playwright capture,
|
|
@@ -271,6 +328,7 @@ await extractThumbnails({
|
|
|
271
328
|
- `readInput(inputPath)` → `{ doc: Doc, container: ContentContainer, markdownDoc?: MarkdownDocument, sourceFormat: FormatId }`
|
|
272
329
|
- `MemoryContentContainer` (re-export from `@bendyline/squisq/storage`)
|
|
273
330
|
- `VideoQuality`, `VideoOrientation` types (re-exports from `@bendyline/squisq-video`)
|
|
331
|
+
- `DASHBOARD_RESOLUTIONS`, `DEFAULT_DASHBOARD_RESOLUTION`, `resolveDashboardDimensions`, `validateDashboardImageDimensions` (re-exports from `@bendyline/squisq-video`)
|
|
274
332
|
|
|
275
333
|
See the full [API Reference](https://github.com/bendyline/squisq/blob/main/docs/API.md#bendylinesquisq-cli) for all types and options.
|
|
276
334
|
|
package/THIRD_PARTY_LICENSES.txt
CHANGED
|
@@ -102,7 +102,7 @@ COMPONENTS
|
|
|
102
102
|
- mdast-util-phrasing@4.1.0 (MIT); files: license
|
|
103
103
|
- mdast-util-to-markdown@2.1.2 (MIT); files: license
|
|
104
104
|
- mdast-util-to-string@4.0.0 (MIT); files: license
|
|
105
|
-
- mermaid@11.16.
|
|
105
|
+
- mermaid@11.16.1 (MIT); files: LICENSE
|
|
106
106
|
- micromark-core-commonmark@2.0.3 (MIT); files: license
|
|
107
107
|
- micromark-extension-directive@3.0.2 (MIT); files: license
|
|
108
108
|
- micromark-extension-frontmatter@2.0.0 (MIT); files: license
|
|
@@ -2987,7 +2987,7 @@ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
|
2987
2987
|
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
2988
2988
|
|
|
2989
2989
|
==============================================================================
|
|
2990
|
-
mermaid@11.16.
|
|
2990
|
+
mermaid@11.16.1 - LICENSE
|
|
2991
2991
|
==============================================================================
|
|
2992
2992
|
The MIT License (MIT)
|
|
2993
2993
|
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import {
|
|
3
3
|
CapturedFrameBudgetError,
|
|
4
4
|
ConversionError,
|
|
5
|
+
DASHBOARD_RESOLUTIONS,
|
|
6
|
+
DEFAULT_DASHBOARD_RESOLUTION,
|
|
5
7
|
MAX_CAPTURED_FRAME_BYTES,
|
|
6
8
|
MemoryContentContainer,
|
|
7
9
|
convert,
|
|
@@ -9,9 +11,12 @@ import {
|
|
|
9
11
|
extractThumbnails,
|
|
10
12
|
prepareConversion,
|
|
11
13
|
readInput,
|
|
14
|
+
renderDocToDashboardPng,
|
|
12
15
|
renderDocToGif,
|
|
13
|
-
renderDocToMp4
|
|
14
|
-
|
|
16
|
+
renderDocToMp4,
|
|
17
|
+
resolveDashboardDimensions,
|
|
18
|
+
validateDashboardImageDimensions
|
|
19
|
+
} from "./chunk-2ZJW7F62.js";
|
|
15
20
|
import {
|
|
16
21
|
GIF_EXPORT_DEFAULTS,
|
|
17
22
|
framesToGifNative,
|
|
@@ -22,6 +27,8 @@ import {
|
|
|
22
27
|
export {
|
|
23
28
|
CapturedFrameBudgetError,
|
|
24
29
|
ConversionError,
|
|
30
|
+
DASHBOARD_RESOLUTIONS,
|
|
31
|
+
DEFAULT_DASHBOARD_RESOLUTION,
|
|
25
32
|
GIF_EXPORT_DEFAULTS,
|
|
26
33
|
MAX_CAPTURED_FRAME_BYTES,
|
|
27
34
|
MemoryContentContainer,
|
|
@@ -34,6 +41,9 @@ export {
|
|
|
34
41
|
framesToMp4NativeBytes,
|
|
35
42
|
prepareConversion,
|
|
36
43
|
readInput,
|
|
44
|
+
renderDocToDashboardPng,
|
|
37
45
|
renderDocToGif,
|
|
38
|
-
renderDocToMp4
|
|
46
|
+
renderDocToMp4,
|
|
47
|
+
resolveDashboardDimensions,
|
|
48
|
+
validateDashboardImageDimensions
|
|
39
49
|
};
|
package/dist/api.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Doc } from '@bendyline/squisq/schemas';
|
|
2
|
+
import { DashboardStyleId } from '@bendyline/squisq/doc';
|
|
2
3
|
import { ContentContainer } from '@bendyline/squisq/storage';
|
|
3
4
|
export { MemoryContentContainer } from '@bendyline/squisq/storage';
|
|
4
|
-
import { VideoQuality, VideoOrientation, GifDither, VideoExportOptions } from '@bendyline/squisq-video';
|
|
5
|
-
export { GifDither, VideoOrientation, VideoQuality } from '@bendyline/squisq-video';
|
|
5
|
+
import { VideoQuality, VideoOrientation, GifDither, DashboardResolutionId, VideoExportOptions } from '@bendyline/squisq-video';
|
|
6
|
+
export { DASHBOARD_RESOLUTIONS, DEFAULT_DASHBOARD_RESOLUTION, DashboardResolutionId, DashboardResolutionPreset, GifDither, VideoOrientation, VideoQuality, resolveDashboardDimensions, validateDashboardImageDimensions } from '@bendyline/squisq-video';
|
|
6
7
|
import { FormatRegistry, FormatId, ConvertOptions, ConvertSource, ConversionResult, PreparedConversion } from '@bendyline/squisq-formats';
|
|
7
8
|
export { ConversionError, ConversionErrorCode, ConversionErrorOptions, ConversionResult, ConvertOptions, ConvertSource, FormatDefinition, FormatId, FormatRegistry, NormalizedInput, PreparedConversion, PreparedExportOptions } from '@bendyline/squisq-formats';
|
|
8
9
|
import { MarkdownDocument } from '@bendyline/squisq/markdown';
|
|
@@ -42,6 +43,22 @@ interface GifFormatOptions {
|
|
|
42
43
|
dither?: GifDither;
|
|
43
44
|
bayerScale?: number;
|
|
44
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Per-export options for `convert(..., 'png')` — the Dashboard image.
|
|
48
|
+
* `resolution` names a preset; explicit `width`/`height` (both required
|
|
49
|
+
* together) win over it. `layout`/`style`/`title` override the doc's
|
|
50
|
+
* dashboard frontmatter.
|
|
51
|
+
*/
|
|
52
|
+
interface PngFormatOptions {
|
|
53
|
+
onProgress?: (phase: string, percent: number) => void;
|
|
54
|
+
resolution?: DashboardResolutionId;
|
|
55
|
+
width?: number;
|
|
56
|
+
height?: number;
|
|
57
|
+
layout?: string;
|
|
58
|
+
/** Cell style variant; unset defers to the doc's own frontmatter. */
|
|
59
|
+
style?: DashboardStyleId;
|
|
60
|
+
title?: boolean;
|
|
61
|
+
}
|
|
45
62
|
/**
|
|
46
63
|
* Build the CLI's format registry: every built-in format from
|
|
47
64
|
* `@bendyline/squisq-formats` plus the CLI-only rendered-media exporters.
|
|
@@ -218,6 +235,7 @@ type CliConvertOptions = Omit<ConvertOptions, 'formatOptions'> & {
|
|
|
218
235
|
formatOptions?: ConvertOptions['formatOptions'] & {
|
|
219
236
|
mp4?: Mp4FormatOptions;
|
|
220
237
|
gif?: GifFormatOptions;
|
|
238
|
+
png?: PngFormatOptions;
|
|
221
239
|
};
|
|
222
240
|
};
|
|
223
241
|
|
|
@@ -319,6 +337,50 @@ interface RenderDocToGifResult extends RenderDocToMp4Result {
|
|
|
319
337
|
/** Non-fatal fidelity warnings, including GIF's lack of audio. */
|
|
320
338
|
warnings: string[];
|
|
321
339
|
}
|
|
340
|
+
/** Options for {@link renderDocToDashboardPng}. */
|
|
341
|
+
interface RenderDashboardPngOptions {
|
|
342
|
+
/** Abort the render; rejects with the signal's reason. */
|
|
343
|
+
signal?: AbortSignal;
|
|
344
|
+
/** Optional output file; bytes are always returned. */
|
|
345
|
+
outputPath?: string;
|
|
346
|
+
/** Named resolution preset (default `'fhd'`, 1920×1080). */
|
|
347
|
+
resolution?: DashboardResolutionId;
|
|
348
|
+
/** Custom pixel width; requires `height` and excludes `resolution`. */
|
|
349
|
+
width?: number;
|
|
350
|
+
/** Custom pixel height; requires `width` and excludes `resolution`. */
|
|
351
|
+
height?: number;
|
|
352
|
+
/** Dashboard layout id, or `'auto'` for the block-count pick (default). */
|
|
353
|
+
layout?: string;
|
|
354
|
+
/**
|
|
355
|
+
* Render the document-title band. Leave unset to defer to the doc's own
|
|
356
|
+
* `squisq-dashboard-title` frontmatter (which itself defaults on).
|
|
357
|
+
*/
|
|
358
|
+
title?: boolean;
|
|
359
|
+
/**
|
|
360
|
+
* Cell style variant (`basic` | `card` | `panel` | `accent`). Default:
|
|
361
|
+
* the document's own `squisq-dashboard-style` setting.
|
|
362
|
+
*/
|
|
363
|
+
style?: DashboardStyleId;
|
|
364
|
+
/** Host-supplied title fallback when the doc has no frontmatter title. */
|
|
365
|
+
documentTitle?: string;
|
|
366
|
+
onProgress?: (phase: string, percent: number) => void;
|
|
367
|
+
}
|
|
368
|
+
/** Result returned by {@link renderDocToDashboardPng}. */
|
|
369
|
+
interface RenderDashboardPngResult {
|
|
370
|
+
bytes: Uint8Array;
|
|
371
|
+
width: number;
|
|
372
|
+
height: number;
|
|
373
|
+
/** Absolute path written, when `outputPath` was requested. */
|
|
374
|
+
outputPath?: string;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Render a Doc's Dashboard rendition to a single PNG image.
|
|
378
|
+
*
|
|
379
|
+
* Needs Playwright Chromium only — unlike the video paths there is no
|
|
380
|
+
* ffmpeg involvement. Apply a theme upstream (`{ ...doc, themeId }`), the
|
|
381
|
+
* same convention `renderDocToMp4`/`renderDocToGif` use.
|
|
382
|
+
*/
|
|
383
|
+
declare function renderDocToDashboardPng(doc: Doc, container: ContentContainer, options?: RenderDashboardPngOptions): Promise<RenderDashboardPngResult>;
|
|
322
384
|
/** Render a Doc + media container to an MP4 video file. */
|
|
323
385
|
declare function renderDocToMp4(doc: Doc, container: ContentContainer, options: RenderDocToMp4Options): Promise<RenderDocToMp4Result>;
|
|
324
386
|
/** Render a Doc + media container to a silent animated GIF. */
|
|
@@ -355,4 +417,4 @@ interface ExtractThumbnailsOptions {
|
|
|
355
417
|
*/
|
|
356
418
|
declare function extractThumbnails(options: ExtractThumbnailsOptions): Promise<void>;
|
|
357
419
|
|
|
358
|
-
export { CapturedFrameBudgetError, type CliConvertOptions, type ExtractThumbnailsOptions, GIF_EXPORT_DEFAULTS, type GifExportOptions, type GifFormatOptions, MAX_CAPTURED_FRAME_BYTES, type Mp4FormatOptions, type NativeVideoExportOptions, type ReadInputResult, type RenderDocToGifOptions, type RenderDocToGifResult, type RenderDocToMp4Options, type RenderDocToMp4Result, type ThumbnailSpec, convert, createCliRegistry, extractThumbnails, framesToGifNative, framesToGifNativeBytes, framesToMp4Native, framesToMp4NativeBytes, prepareConversion, readInput, renderDocToGif, renderDocToMp4 };
|
|
420
|
+
export { CapturedFrameBudgetError, type CliConvertOptions, type ExtractThumbnailsOptions, GIF_EXPORT_DEFAULTS, type GifExportOptions, type GifFormatOptions, MAX_CAPTURED_FRAME_BYTES, type Mp4FormatOptions, type NativeVideoExportOptions, type PngFormatOptions, type ReadInputResult, type RenderDashboardPngOptions, type RenderDashboardPngResult, type RenderDocToGifOptions, type RenderDocToGifResult, type RenderDocToMp4Options, type RenderDocToMp4Result, type ThumbnailSpec, convert, createCliRegistry, extractThumbnails, framesToGifNative, framesToGifNativeBytes, framesToMp4Native, framesToMp4NativeBytes, prepareConversion, readInput, renderDocToDashboardPng, renderDocToGif, renderDocToMp4 };
|
package/dist/api.js
CHANGED
|
@@ -8,11 +8,12 @@ import {
|
|
|
8
8
|
} from "./chunk-Z3BHD4AF.js";
|
|
9
9
|
|
|
10
10
|
// src/api.ts
|
|
11
|
-
import { readFile as readFile3 } from "fs/promises";
|
|
11
|
+
import { mkdir, readFile as readFile3, writeFile } from "fs/promises";
|
|
12
|
+
import { dirname as dirname2, resolve as resolvePath } from "path";
|
|
12
13
|
import { resolveMediaSchedule as resolveMediaSchedule2 } from "@bendyline/squisq/schemas";
|
|
13
14
|
import { flattenBlocks as flattenBlocks2 } from "@bendyline/squisq/doc";
|
|
14
15
|
import { ffmpegGifOutputArgs, generateRenderHtml } from "@bendyline/squisq-video";
|
|
15
|
-
import { resolveDimensions } from "@bendyline/squisq-video";
|
|
16
|
+
import { resolveDashboardDimensions, resolveDimensions } from "@bendyline/squisq-video";
|
|
16
17
|
import {
|
|
17
18
|
convert as formatsConvert,
|
|
18
19
|
prepareConversion as formatsPrepareConversion
|
|
@@ -97,13 +98,13 @@ async function buildMixedAudioTrack(doc, container, ffmpegPath, coverPreRoll, si
|
|
|
97
98
|
return mixTimelineClips(ffmpegPath, usable, signal);
|
|
98
99
|
}
|
|
99
100
|
async function mixTimelineClips(ffmpegPath, usable, signal) {
|
|
100
|
-
const { writeFile, readFile: readFile4, mkdir, rm: rm2 } = await import("fs/promises");
|
|
101
|
+
const { writeFile: writeFile2, readFile: readFile4, mkdir: mkdir2, rm: rm2 } = await import("fs/promises");
|
|
101
102
|
const { join: join3 } = await import("path");
|
|
102
103
|
const { tmpdir: tmpdir2 } = await import("os");
|
|
103
104
|
const { randomBytes: randomBytes2 } = await import("crypto");
|
|
104
105
|
signal?.throwIfAborted();
|
|
105
106
|
const workDir = join3(tmpdir2(), `squisq-audio-mix-${randomBytes2(8).toString("hex")}`);
|
|
106
|
-
await
|
|
107
|
+
await mkdir2(workDir, { recursive: true });
|
|
107
108
|
const ms = (s) => Math.max(0, Math.round(s * 1e3));
|
|
108
109
|
try {
|
|
109
110
|
signal?.throwIfAborted();
|
|
@@ -113,7 +114,7 @@ async function mixTimelineClips(ffmpegPath, usable, signal) {
|
|
|
113
114
|
for (const { clip, buffer } of usable) {
|
|
114
115
|
signal?.throwIfAborted();
|
|
115
116
|
const p = join3(workDir, `clip-${inputs.length}.mp3`);
|
|
116
|
-
await
|
|
117
|
+
await writeFile2(p, new Uint8Array(buffer));
|
|
117
118
|
signal?.throwIfAborted();
|
|
118
119
|
const i = inputs.push(p) - 1;
|
|
119
120
|
const delayMs = ms(clip.startSec);
|
|
@@ -407,10 +408,38 @@ function gifFormat() {
|
|
|
407
408
|
}
|
|
408
409
|
};
|
|
409
410
|
}
|
|
411
|
+
function pngFormat() {
|
|
412
|
+
return {
|
|
413
|
+
id: "png",
|
|
414
|
+
templateAnnotationHandling: "rendered",
|
|
415
|
+
label: "Dashboard Image",
|
|
416
|
+
mimeType: "image/png",
|
|
417
|
+
extensions: [".png"],
|
|
418
|
+
async exportDoc(input, options) {
|
|
419
|
+
options.signal?.throwIfAborted();
|
|
420
|
+
const pngOpts = options.formatOptions?.png ?? {};
|
|
421
|
+
const { renderDocToDashboardPng: renderDocToDashboardPng2 } = await import("./api.js");
|
|
422
|
+
const result = await renderDocToDashboardPng2(input.doc, input.container, {
|
|
423
|
+
resolution: pngOpts.resolution,
|
|
424
|
+
width: pngOpts.width,
|
|
425
|
+
height: pngOpts.height,
|
|
426
|
+
layout: pngOpts.layout,
|
|
427
|
+
style: pngOpts.style,
|
|
428
|
+
title: pngOpts.title,
|
|
429
|
+
documentTitle: options.title ?? input.baseName,
|
|
430
|
+
signal: options.signal,
|
|
431
|
+
onProgress: pngOpts.onProgress
|
|
432
|
+
});
|
|
433
|
+
options.signal?.throwIfAborted();
|
|
434
|
+
return { bytes: result.bytes, mimeType: "image/png", suggestedFilename: "", warnings: [] };
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
}
|
|
410
438
|
function createCliRegistry() {
|
|
411
439
|
const registry = defaultRegistry();
|
|
412
440
|
registry.register(mp4Format());
|
|
413
441
|
registry.register(gifFormat());
|
|
442
|
+
registry.register(pngFormat());
|
|
414
443
|
return registry;
|
|
415
444
|
}
|
|
416
445
|
|
|
@@ -827,6 +856,12 @@ function throwIfAborted(signal) {
|
|
|
827
856
|
}
|
|
828
857
|
|
|
829
858
|
// src/api.ts
|
|
859
|
+
import {
|
|
860
|
+
DASHBOARD_RESOLUTIONS,
|
|
861
|
+
DEFAULT_DASHBOARD_RESOLUTION,
|
|
862
|
+
resolveDashboardDimensions as resolveDashboardDimensions2,
|
|
863
|
+
validateDashboardImageDimensions
|
|
864
|
+
} from "@bendyline/squisq-video";
|
|
830
865
|
import { ConversionError } from "@bendyline/squisq-formats";
|
|
831
866
|
var playerBundlePromise;
|
|
832
867
|
var fullPlayerBundlePromise;
|
|
@@ -866,17 +901,18 @@ async function prepareConversion(source, options = {}) {
|
|
|
866
901
|
...options
|
|
867
902
|
});
|
|
868
903
|
}
|
|
869
|
-
async function
|
|
870
|
-
const {
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
904
|
+
async function withRenderPage(doc, container, options, fn) {
|
|
905
|
+
const {
|
|
906
|
+
signal,
|
|
907
|
+
width,
|
|
908
|
+
height,
|
|
909
|
+
includeAudio,
|
|
910
|
+
captionStyle,
|
|
911
|
+
animationsEnabled,
|
|
912
|
+
displayMode,
|
|
913
|
+
dashboard,
|
|
914
|
+
onProgress
|
|
915
|
+
} = options;
|
|
880
916
|
onProgress?.("collecting media", 0);
|
|
881
917
|
signal?.throwIfAborted();
|
|
882
918
|
const { collectImagePaths } = await import("@bendyline/squisq-formats/html");
|
|
@@ -893,14 +929,16 @@ async function captureDocFrames(doc, container, options) {
|
|
|
893
929
|
}
|
|
894
930
|
}
|
|
895
931
|
const audio = /* @__PURE__ */ new Map();
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
932
|
+
if (includeAudio) {
|
|
933
|
+
for (const seg of doc.audio?.segments ?? []) {
|
|
934
|
+
signal?.throwIfAborted();
|
|
935
|
+
const data = await container.readFile(seg.src);
|
|
936
|
+
signal?.throwIfAborted();
|
|
937
|
+
if (data) {
|
|
938
|
+
budget.admit(seg.src, data);
|
|
939
|
+
audio.set(seg.src, data);
|
|
940
|
+
audio.set(seg.name, data);
|
|
941
|
+
}
|
|
904
942
|
}
|
|
905
943
|
}
|
|
906
944
|
const mediaSrcs = new Set(resolveMediaSchedule2(doc).map((clip) => clip.src));
|
|
@@ -930,7 +968,9 @@ async function captureDocFrames(doc, container, options) {
|
|
|
930
968
|
width,
|
|
931
969
|
height,
|
|
932
970
|
captionStyle,
|
|
933
|
-
animationsEnabled
|
|
971
|
+
animationsEnabled,
|
|
972
|
+
displayMode,
|
|
973
|
+
dashboard
|
|
934
974
|
});
|
|
935
975
|
onProgress?.("launching browser", 15);
|
|
936
976
|
signal?.throwIfAborted();
|
|
@@ -956,7 +996,6 @@ async function captureDocFrames(doc, container, options) {
|
|
|
956
996
|
}
|
|
957
997
|
signal?.addEventListener("abort", handleAbort, { once: true });
|
|
958
998
|
let renderAPI = null;
|
|
959
|
-
const capturedFrames = new CapturedFrameCollector();
|
|
960
999
|
try {
|
|
961
1000
|
const page = await browser.newPage({ viewport: { width, height } });
|
|
962
1001
|
const pageErrors = [];
|
|
@@ -997,58 +1036,135 @@ Page errors:
|
|
|
997
1036
|
return api;
|
|
998
1037
|
});
|
|
999
1038
|
signal?.throwIfAborted();
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1039
|
+
return await fn({ page, renderAPI });
|
|
1040
|
+
} finally {
|
|
1041
|
+
signal?.removeEventListener("abort", handleAbort);
|
|
1042
|
+
await renderAPI?.dispose().catch(() => void 0);
|
|
1043
|
+
await browser.close().catch(() => void 0);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
async function captureDocFrames(doc, container, options) {
|
|
1047
|
+
const { fps, width, height, captionStyle, coverPreRoll, animationsEnabled, onProgress, signal } = options;
|
|
1048
|
+
signal?.throwIfAborted();
|
|
1049
|
+
resolveAppliedCoverPreRoll(coverPreRoll, true);
|
|
1050
|
+
const ffmpegPath = (await detectFfmpegDetailed(signal))?.path ?? null;
|
|
1051
|
+
signal?.throwIfAborted();
|
|
1052
|
+
if (!ffmpegPath) {
|
|
1053
|
+
throw new Error(
|
|
1054
|
+
"ffmpeg is required but not found in PATH.\nInstall it with:\n macOS: brew install ffmpeg\n Ubuntu: sudo apt install ffmpeg\n Windows: winget install ffmpeg\nOr: npm install ffmpeg-static, or set SQUISQ_FFMPEG to an ffmpeg binary."
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
const capturedFrames = new CapturedFrameCollector();
|
|
1058
|
+
try {
|
|
1059
|
+
return await withRenderPage(
|
|
1060
|
+
doc,
|
|
1061
|
+
container,
|
|
1062
|
+
{ signal, width, height, includeAudio: true, captionStyle, animationsEnabled, onProgress },
|
|
1063
|
+
async ({ page, renderAPI }) => {
|
|
1064
|
+
const docDuration = await renderAPI.evaluate((api) => api.getDuration());
|
|
1065
|
+
signal?.throwIfAborted();
|
|
1066
|
+
if (docDuration <= 0) throw new Error("Document has zero duration \u2014 nothing to render");
|
|
1067
|
+
const hasCover = coverPreRoll > 0 ? await renderAPI.evaluate((api) => api.hasCoverBlock()) : false;
|
|
1068
|
+
const appliedCoverPreRoll = resolveAppliedCoverPreRoll(coverPreRoll, hasCover);
|
|
1069
|
+
const storyFrameCount = Math.ceil(docDuration * fps);
|
|
1070
|
+
const preRollFrameCount = Math.ceil(appliedCoverPreRoll * fps);
|
|
1071
|
+
const totalFrames = preRollFrameCount + storyFrameCount;
|
|
1072
|
+
onProgress?.("capturing frames", 20);
|
|
1033
1073
|
capturedFrames.throwIfAborted(signal);
|
|
1074
|
+
if (preRollFrameCount > 0) {
|
|
1075
|
+
capturedFrames.throwIfAborted(signal);
|
|
1076
|
+
await renderAPI.evaluate((api) => api.showCover());
|
|
1077
|
+
await page.waitForTimeout(100);
|
|
1078
|
+
capturedFrames.throwIfAborted(signal);
|
|
1079
|
+
const coverFrame = await page.screenshot({ type: "png" });
|
|
1080
|
+
capturedFrames.throwIfAborted(signal);
|
|
1081
|
+
capturedFrames.append(coverFrame, preRollFrameCount);
|
|
1082
|
+
await renderAPI.evaluate((api) => api.hideCover());
|
|
1083
|
+
}
|
|
1084
|
+
const frameInterval = 1 / fps;
|
|
1085
|
+
for (let i = 0; i < storyFrameCount; i++) {
|
|
1086
|
+
capturedFrames.throwIfAborted(signal);
|
|
1087
|
+
const time = i * frameInterval;
|
|
1088
|
+
await renderAPI.evaluate((api, t) => api.seekTo(t), time);
|
|
1089
|
+
const frame = await page.screenshot({ type: "png" });
|
|
1090
|
+
capturedFrames.throwIfAborted(signal);
|
|
1091
|
+
capturedFrames.append(frame);
|
|
1092
|
+
if (i % Math.max(1, Math.floor(fps / 2)) === 0 || i === storyFrameCount - 1) {
|
|
1093
|
+
onProgress?.(
|
|
1094
|
+
"capturing frames",
|
|
1095
|
+
20 + Math.round(capturedFrames.frameCount / totalFrames * 60)
|
|
1096
|
+
);
|
|
1097
|
+
capturedFrames.throwIfAborted(signal);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
return {
|
|
1101
|
+
frames: capturedFrames.release(),
|
|
1102
|
+
totalDuration: docDuration + appliedCoverPreRoll,
|
|
1103
|
+
appliedCoverPreRoll,
|
|
1104
|
+
ffmpegPath
|
|
1105
|
+
};
|
|
1034
1106
|
}
|
|
1035
|
-
|
|
1036
|
-
return {
|
|
1037
|
-
frames: capturedFrames.release(),
|
|
1038
|
-
totalDuration: docDuration + appliedCoverPreRoll,
|
|
1039
|
-
appliedCoverPreRoll,
|
|
1040
|
-
ffmpegPath
|
|
1041
|
-
};
|
|
1107
|
+
);
|
|
1042
1108
|
} catch (err) {
|
|
1043
1109
|
capturedFrames.clear();
|
|
1044
1110
|
signal?.throwIfAborted();
|
|
1045
1111
|
throw err;
|
|
1046
|
-
} finally {
|
|
1047
|
-
signal?.removeEventListener("abort", handleAbort);
|
|
1048
|
-
await renderAPI?.dispose().catch(() => void 0);
|
|
1049
|
-
await browser.close().catch(() => void 0);
|
|
1050
1112
|
}
|
|
1051
1113
|
}
|
|
1114
|
+
async function renderDocToDashboardPng(doc, container, options = {}) {
|
|
1115
|
+
const {
|
|
1116
|
+
signal,
|
|
1117
|
+
outputPath,
|
|
1118
|
+
resolution,
|
|
1119
|
+
width,
|
|
1120
|
+
height,
|
|
1121
|
+
layout,
|
|
1122
|
+
title,
|
|
1123
|
+
style,
|
|
1124
|
+
documentTitle,
|
|
1125
|
+
onProgress
|
|
1126
|
+
} = options;
|
|
1127
|
+
signal?.throwIfAborted();
|
|
1128
|
+
const dimensions = resolveDashboardDimensions({ resolution, width, height });
|
|
1129
|
+
const bytes = await withRenderPage(
|
|
1130
|
+
doc,
|
|
1131
|
+
container,
|
|
1132
|
+
{
|
|
1133
|
+
signal,
|
|
1134
|
+
width: dimensions.width,
|
|
1135
|
+
height: dimensions.height,
|
|
1136
|
+
includeAudio: false,
|
|
1137
|
+
animationsEnabled: false,
|
|
1138
|
+
displayMode: "dashboard",
|
|
1139
|
+
dashboard: { layout, title, style, documentTitle },
|
|
1140
|
+
onProgress
|
|
1141
|
+
},
|
|
1142
|
+
async ({ page, renderAPI }) => {
|
|
1143
|
+
onProgress?.("rendering dashboard", 50);
|
|
1144
|
+
signal?.throwIfAborted();
|
|
1145
|
+
await renderAPI.evaluate((api) => api.seekTo(0));
|
|
1146
|
+
signal?.throwIfAborted();
|
|
1147
|
+
await page.waitForTimeout(100);
|
|
1148
|
+
signal?.throwIfAborted();
|
|
1149
|
+
onProgress?.("capturing image", 85);
|
|
1150
|
+
return await page.screenshot({ type: "png" });
|
|
1151
|
+
}
|
|
1152
|
+
);
|
|
1153
|
+
let writtenPath;
|
|
1154
|
+
if (outputPath) {
|
|
1155
|
+
const absolute = resolvePath(outputPath);
|
|
1156
|
+
await mkdir(dirname2(absolute), { recursive: true });
|
|
1157
|
+
await writeFile(absolute, bytes);
|
|
1158
|
+
writtenPath = absolute;
|
|
1159
|
+
}
|
|
1160
|
+
onProgress?.("done", 100);
|
|
1161
|
+
return {
|
|
1162
|
+
bytes,
|
|
1163
|
+
width: dimensions.width,
|
|
1164
|
+
height: dimensions.height,
|
|
1165
|
+
...writtenPath ? { outputPath: writtenPath } : {}
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1052
1168
|
async function renderDocToMp4(doc, container, options) {
|
|
1053
1169
|
options.signal?.throwIfAborted();
|
|
1054
1170
|
const fps = options.fps ?? 30;
|
|
@@ -1231,6 +1347,8 @@ async function extractThumbnails(options) {
|
|
|
1231
1347
|
export {
|
|
1232
1348
|
CapturedFrameBudgetError,
|
|
1233
1349
|
ConversionError,
|
|
1350
|
+
DASHBOARD_RESOLUTIONS,
|
|
1351
|
+
DEFAULT_DASHBOARD_RESOLUTION,
|
|
1234
1352
|
GIF_EXPORT_DEFAULTS,
|
|
1235
1353
|
MAX_CAPTURED_FRAME_BYTES,
|
|
1236
1354
|
MemoryContentContainer2 as MemoryContentContainer,
|
|
@@ -1243,6 +1361,9 @@ export {
|
|
|
1243
1361
|
framesToMp4NativeBytes,
|
|
1244
1362
|
prepareConversion,
|
|
1245
1363
|
readInput,
|
|
1364
|
+
renderDocToDashboardPng,
|
|
1246
1365
|
renderDocToGif,
|
|
1247
|
-
renderDocToMp4
|
|
1366
|
+
renderDocToMp4,
|
|
1367
|
+
resolveDashboardDimensions2 as resolveDashboardDimensions,
|
|
1368
|
+
validateDashboardImageDimensions
|
|
1248
1369
|
};
|