@bendyline/squisq-video 2.2.11 → 2.3.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/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Doc, Theme, VideoPresentation, VideoPipSize, VideoPipShape, VideoPipPosition } from '@bendyline/squisq/schemas';
1
+ import { Doc, Theme, VideoPresentation, VideoPipSize, VideoPipShape, VideoPipPosition, ViewportPreset } from '@bendyline/squisq/schemas';
2
2
  export { fetchFile } from '@ffmpeg/util';
3
3
 
4
4
  /**
@@ -260,6 +260,26 @@ interface RenderHtmlOptions {
260
260
  * Defaults to true. Embedded/timed media and document timing are unaffected.
261
261
  */
262
262
  animationsEnabled?: boolean;
263
+ /**
264
+ * Player rendition mounted in the capture page (default 'slideshow').
265
+ * 'dashboard' renders the one-canvas dashboard projection for
266
+ * single-frame image capture.
267
+ */
268
+ displayMode?: 'slideshow' | 'dashboard';
269
+ /** Dashboard-mode options (ignored unless `displayMode` is 'dashboard'). */
270
+ dashboard?: {
271
+ /** Layout id or 'auto'. Overrides doc frontmatter. */
272
+ layout?: string;
273
+ /** Title-band override. Overrides doc frontmatter. */
274
+ title?: boolean;
275
+ /**
276
+ * Cell style variant ('basic' | 'card' | 'panel' | 'accent'). Overrides
277
+ * doc frontmatter; colors always come from the active theme.
278
+ */
279
+ style?: string;
280
+ /** Host-supplied title fallback (typically the file name). */
281
+ documentTitle?: string;
282
+ };
263
283
  }
264
284
  /**
265
285
  * Generate a self-contained HTML document for headless video frame capture.
@@ -273,6 +293,122 @@ interface RenderHtmlOptions {
273
293
  */
274
294
  declare function generateRenderHtml(doc: Doc, options: RenderHtmlOptions): string;
275
295
 
296
+ /**
297
+ * Dashboard image export presets and dimension helpers.
298
+ *
299
+ * Lives in @bendyline/squisq-video (browser-pure, depended on by both the
300
+ * CLI and the browser export UI) so the `squisq image` command and the
301
+ * DashboardImageExportModal share one resolution table and one validator.
302
+ *
303
+ * These are EXPORT resolutions (physical pixels), distinct from core's
304
+ * `VIEWPORT_PRESETS` (virtual design-space viewports) — each preset names
305
+ * the viewport family its aspect ratio implies so layouts pick the right
306
+ * orientation variant. Validation follows the cover-image ruleset (whole
307
+ * pixels, sane bounds, megapixel cap), NOT the H.264 even-dimension rule:
308
+ * that constraint exists for yuv420p video encoding and has no meaning
309
+ * for a PNG.
310
+ */
311
+
312
+ interface DashboardResolutionPreset {
313
+ id: string;
314
+ label: string;
315
+ width: number;
316
+ height: number;
317
+ /** The `VIEWPORT_PRESETS` family this aspect ratio implies. */
318
+ family: ViewportPreset;
319
+ }
320
+ /** Named export resolutions offered by the CLI and the export dialog. */
321
+ declare const DASHBOARD_RESOLUTIONS: readonly [{
322
+ readonly id: "hd";
323
+ readonly label: "HD 1280×720";
324
+ readonly width: 1280;
325
+ readonly height: 720;
326
+ readonly family: "landscape";
327
+ }, {
328
+ readonly id: "fhd";
329
+ readonly label: "Full HD 1920×1080";
330
+ readonly width: 1920;
331
+ readonly height: 1080;
332
+ readonly family: "landscape";
333
+ }, {
334
+ readonly id: "4k";
335
+ readonly label: "4K UHD 3840×2160";
336
+ readonly width: 3840;
337
+ readonly height: 2160;
338
+ readonly family: "landscape";
339
+ }, {
340
+ readonly id: "square";
341
+ readonly label: "Square 1080×1080";
342
+ readonly width: 1080;
343
+ readonly height: 1080;
344
+ readonly family: "square";
345
+ }, {
346
+ readonly id: "square-2k";
347
+ readonly label: "Square 2160×2160";
348
+ readonly width: 2160;
349
+ readonly height: 2160;
350
+ readonly family: "square";
351
+ }, {
352
+ readonly id: "portrait";
353
+ readonly label: "Portrait 1080×1920";
354
+ readonly width: 1080;
355
+ readonly height: 1920;
356
+ readonly family: "portrait";
357
+ }, {
358
+ readonly id: "portrait-4k";
359
+ readonly label: "Portrait 4K 2160×3840";
360
+ readonly width: 2160;
361
+ readonly height: 3840;
362
+ readonly family: "portrait";
363
+ }, {
364
+ readonly id: "standard";
365
+ readonly label: "4:3 1440×1080";
366
+ readonly width: 1440;
367
+ readonly height: 1080;
368
+ readonly family: "standard";
369
+ }];
370
+ type DashboardResolutionId = (typeof DASHBOARD_RESOLUTIONS)[number]['id'];
371
+ declare const DEFAULT_DASHBOARD_RESOLUTION: DashboardResolutionId;
372
+ /** Minimum edge length for a dashboard image, in pixels. */
373
+ declare const MIN_DASHBOARD_IMAGE_DIMENSION = 64;
374
+ /** Maximum edge length for a dashboard image, in pixels (8K width). */
375
+ declare const MAX_DASHBOARD_IMAGE_DIMENSION = 7680;
376
+ /** Maximum total pixels (4× the 1080p-based cover-image budget: 7680×4320). */
377
+ declare const MAX_DASHBOARD_IMAGE_PIXELS = 33177600;
378
+ /**
379
+ * Validate custom dashboard-image dimensions. Returns a human-readable
380
+ * problem description, or null when the dimensions are acceptable.
381
+ */
382
+ declare function validateDashboardImageDimensions(width: number, height: number): string | null;
383
+ /**
384
+ * The viewport family whose aspect ratio is nearest to `width`/`height` —
385
+ * a uniform log-ratio comparison over all four `VIEWPORT_PRESETS`
386
+ * families (core's `getViewportOrientation` has no 4:3 notion, so 1440×1080
387
+ * would misreport as plain landscape there).
388
+ */
389
+ declare function dashboardFamilyForDimensions(width: number, height: number): ViewportPreset;
390
+ interface ResolveDashboardDimensionsInput {
391
+ /** Named preset id (default {@link DEFAULT_DASHBOARD_RESOLUTION}). */
392
+ resolution?: string;
393
+ /** Custom width; requires `height` and excludes `resolution`. */
394
+ width?: number;
395
+ /** Custom height; requires `width` and excludes `resolution`. */
396
+ height?: number;
397
+ }
398
+ interface ResolvedDashboardDimensions {
399
+ width: number;
400
+ height: number;
401
+ family: ViewportPreset;
402
+ }
403
+ /**
404
+ * Resolve the export dimensions from a preset id or explicit pixels — the
405
+ * single resolution-logic implementation shared by the programmatic API,
406
+ * the `png` format, and the `squisq image` command. Throws `RangeError`
407
+ * on contradictory or invalid input so callers fail before any expensive
408
+ * rendering starts.
409
+ */
410
+ declare function resolveDashboardDimensions(input?: ResolveDashboardDimensionsInput): ResolvedDashboardDimensions;
411
+
276
412
  /**
277
413
  * FFmpeg argument builders — the single source of truth for translating a
278
414
  * {@link VideoQuality} into ffmpeg CLI flags. Shared verbatim by every
@@ -358,4 +494,4 @@ declare function ffmpegGifOutputArgs(options: GifOutputOptions): string[];
358
494
  */
359
495
  declare function framesToMp4Wasm(frames: Uint8Array[], audio: Uint8Array | null, options?: VideoExportOptions): Promise<EncoderResult>;
360
496
 
361
- export { type AudioTimelineClip, type ComputeAudioTimelineOptions, type EncoderResult, FFMPEG_WASM_SETUP_HINT, type FfmpegWasmLoadConfig, type GifDither, type GifFilterOptions, type GifOutputOptions, ORIENTATION_DIMENSIONS, QUALITY_PRESETS, type QualityPreset, type RenderHtmlOptions, type VideoExportOptions, type VideoOrientation, type VideoQuality, audioBitrateArg, bitrateForQuality, computeAudioTimeline, ffmpegAudioMuxArgs, ffmpegGifFilterGraph, ffmpegGifOutputArgs, ffmpegGifPaletteApplicationArgs, ffmpegGifPaletteApplicationGraph, ffmpegGifPaletteGenerationFilter, ffmpegVideoQualityArgs, framesToMp4Wasm, generateRenderHtml, resolveDimensions, resolveFfmpegWasmLoad, validateVideoExportOptions };
497
+ export { type AudioTimelineClip, type ComputeAudioTimelineOptions, DASHBOARD_RESOLUTIONS, DEFAULT_DASHBOARD_RESOLUTION, type DashboardResolutionId, type DashboardResolutionPreset, type EncoderResult, FFMPEG_WASM_SETUP_HINT, type FfmpegWasmLoadConfig, type GifDither, type GifFilterOptions, type GifOutputOptions, MAX_DASHBOARD_IMAGE_DIMENSION, MAX_DASHBOARD_IMAGE_PIXELS, MIN_DASHBOARD_IMAGE_DIMENSION, ORIENTATION_DIMENSIONS, QUALITY_PRESETS, type QualityPreset, type RenderHtmlOptions, type ResolveDashboardDimensionsInput, type ResolvedDashboardDimensions, type VideoExportOptions, type VideoOrientation, type VideoQuality, audioBitrateArg, bitrateForQuality, computeAudioTimeline, dashboardFamilyForDimensions, ffmpegAudioMuxArgs, ffmpegGifFilterGraph, ffmpegGifOutputArgs, ffmpegGifPaletteApplicationArgs, ffmpegGifPaletteApplicationGraph, ffmpegGifPaletteGenerationFilter, ffmpegVideoQualityArgs, framesToMp4Wasm, generateRenderHtml, resolveDashboardDimensions, resolveDimensions, resolveFfmpegWasmLoad, validateDashboardImageDimensions, validateVideoExportOptions };
package/dist/index.js CHANGED
@@ -180,7 +180,9 @@ function generateRenderHtml(doc, options) {
180
180
  pipSize,
181
181
  pipShape,
182
182
  pipPosition,
183
- animationsEnabled = true
183
+ animationsEnabled = true,
184
+ displayMode = "slideshow",
185
+ dashboard
184
186
  } = options;
185
187
  const imageMap = {};
186
188
  if (images) {
@@ -206,7 +208,8 @@ function generateRenderHtml(doc, options) {
206
208
  videoPresentation ? ` videoPresentation: ${JSON.stringify(videoPresentation)}` : null,
207
209
  pipSize ? ` pipSize: ${JSON.stringify(pipSize)}` : null,
208
210
  pipShape ? ` pipShape: ${JSON.stringify(pipShape)}` : null,
209
- pipPosition ? ` pipPosition: ${JSON.stringify(pipPosition)}` : null
211
+ pipPosition ? ` pipPosition: ${JSON.stringify(pipPosition)}` : null,
212
+ displayMode === "dashboard" ? ` dashboard: ${escapeForScript(JSON.stringify(dashboard ?? {}))}` : null
210
213
  ].filter((line) => line !== null);
211
214
  return `<!DOCTYPE html>
212
215
  <html lang="en">
@@ -230,7 +233,7 @@ html,body{margin:0;padding:0;width:${width}px;height:${height}px;overflow:hidden
230
233
  var audio = ${audioMapJson === "null" ? "null" : "JSON.parse(" + JSON.stringify(audioMapJson) + ")"};
231
234
  var root = document.getElementById("squisq-root");
232
235
  SquisqPlayer.mount(root, doc, {
233
- mode: "slideshow",
236
+ mode: ${JSON.stringify(displayMode === "dashboard" ? "dashboard" : "slideshow")},
234
237
  images: images,
235
238
  audio: audio,
236
239
  autoPlay: false,
@@ -244,6 +247,83 @@ ${playerOptionLines.join(",\n")}
244
247
  </html>`;
245
248
  }
246
249
 
250
+ // src/dashboardImage.ts
251
+ import { VIEWPORT_PRESETS } from "@bendyline/squisq/schemas";
252
+ var DASHBOARD_RESOLUTIONS = [
253
+ { id: "hd", label: "HD 1280\xD7720", width: 1280, height: 720, family: "landscape" },
254
+ { id: "fhd", label: "Full HD 1920\xD71080", width: 1920, height: 1080, family: "landscape" },
255
+ { id: "4k", label: "4K UHD 3840\xD72160", width: 3840, height: 2160, family: "landscape" },
256
+ { id: "square", label: "Square 1080\xD71080", width: 1080, height: 1080, family: "square" },
257
+ { id: "square-2k", label: "Square 2160\xD72160", width: 2160, height: 2160, family: "square" },
258
+ { id: "portrait", label: "Portrait 1080\xD71920", width: 1080, height: 1920, family: "portrait" },
259
+ {
260
+ id: "portrait-4k",
261
+ label: "Portrait 4K 2160\xD73840",
262
+ width: 2160,
263
+ height: 3840,
264
+ family: "portrait"
265
+ },
266
+ { id: "standard", label: "4:3 1440\xD71080", width: 1440, height: 1080, family: "standard" }
267
+ ];
268
+ var DEFAULT_DASHBOARD_RESOLUTION = "fhd";
269
+ var MIN_DASHBOARD_IMAGE_DIMENSION = 64;
270
+ var MAX_DASHBOARD_IMAGE_DIMENSION = 7680;
271
+ var MAX_DASHBOARD_IMAGE_PIXELS = 33177600;
272
+ function validateDashboardImageDimensions(width, height) {
273
+ if (!Number.isSafeInteger(width) || !Number.isSafeInteger(height)) {
274
+ return "Width and height must be whole pixel counts.";
275
+ }
276
+ if (width < MIN_DASHBOARD_IMAGE_DIMENSION || height < MIN_DASHBOARD_IMAGE_DIMENSION) {
277
+ return `Each dimension must be at least ${MIN_DASHBOARD_IMAGE_DIMENSION} pixels.`;
278
+ }
279
+ if (width > MAX_DASHBOARD_IMAGE_DIMENSION || height > MAX_DASHBOARD_IMAGE_DIMENSION) {
280
+ return `Each dimension must be at most ${MAX_DASHBOARD_IMAGE_DIMENSION} pixels.`;
281
+ }
282
+ if (width * height > MAX_DASHBOARD_IMAGE_PIXELS) {
283
+ return `The image may not exceed ${MAX_DASHBOARD_IMAGE_PIXELS.toLocaleString("en-US")} total pixels.`;
284
+ }
285
+ return null;
286
+ }
287
+ function dashboardFamilyForDimensions(width, height) {
288
+ const aspect = width / Math.max(1, height);
289
+ let best = "landscape";
290
+ let bestDistance = Number.POSITIVE_INFINITY;
291
+ for (const family of Object.keys(VIEWPORT_PRESETS)) {
292
+ const preset = VIEWPORT_PRESETS[family];
293
+ const distance = Math.abs(Math.log(aspect / (preset.width / preset.height)));
294
+ if (distance < bestDistance) {
295
+ bestDistance = distance;
296
+ best = family;
297
+ }
298
+ }
299
+ return best;
300
+ }
301
+ function resolveDashboardDimensions(input = {}) {
302
+ const hasCustom = input.width !== void 0 || input.height !== void 0;
303
+ if (hasCustom && input.resolution !== void 0) {
304
+ throw new RangeError("Pass either a resolution preset or custom width/height, not both.");
305
+ }
306
+ if (hasCustom) {
307
+ if (input.width === void 0 || input.height === void 0) {
308
+ throw new RangeError("Custom dimensions require both width and height.");
309
+ }
310
+ const problem = validateDashboardImageDimensions(input.width, input.height);
311
+ if (problem) throw new RangeError(problem);
312
+ return {
313
+ width: input.width,
314
+ height: input.height,
315
+ family: dashboardFamilyForDimensions(input.width, input.height)
316
+ };
317
+ }
318
+ const id = input.resolution ?? DEFAULT_DASHBOARD_RESOLUTION;
319
+ const preset = DASHBOARD_RESOLUTIONS.find((entry) => entry.id === id);
320
+ if (!preset) {
321
+ const known = DASHBOARD_RESOLUTIONS.map((entry) => entry.id).join(", ");
322
+ throw new RangeError(`Unknown resolution preset "${id}". Valid presets: ${known}.`);
323
+ }
324
+ return { width: preset.width, height: preset.height, family: preset.family };
325
+ }
326
+
247
327
  // src/ffmpegArgs.ts
248
328
  function ffmpegVideoQualityArgs(quality) {
249
329
  const preset = QUALITY_PRESETS[quality] ?? QUALITY_PRESETS.normal;
@@ -413,12 +493,18 @@ async function framesToMp4Wasm(frames, audio, options = {}) {
413
493
  }
414
494
  }
415
495
  export {
496
+ DASHBOARD_RESOLUTIONS,
497
+ DEFAULT_DASHBOARD_RESOLUTION,
416
498
  FFMPEG_WASM_SETUP_HINT,
499
+ MAX_DASHBOARD_IMAGE_DIMENSION,
500
+ MAX_DASHBOARD_IMAGE_PIXELS,
501
+ MIN_DASHBOARD_IMAGE_DIMENSION,
417
502
  ORIENTATION_DIMENSIONS,
418
503
  QUALITY_PRESETS,
419
504
  audioBitrateArg,
420
505
  bitrateForQuality,
421
506
  computeAudioTimeline,
507
+ dashboardFamilyForDimensions,
422
508
  fetchFile,
423
509
  ffmpegAudioMuxArgs,
424
510
  ffmpegGifFilterGraph,
@@ -429,7 +515,9 @@ export {
429
515
  ffmpegVideoQualityArgs,
430
516
  framesToMp4Wasm,
431
517
  generateRenderHtml,
518
+ resolveDashboardDimensions,
432
519
  resolveDimensions,
433
520
  resolveFfmpegWasmLoad,
521
+ validateDashboardImageDimensions,
434
522
  validateVideoExportOptions
435
523
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bendyline/squisq-video",
3
- "version": "2.2.11",
3
+ "version": "2.3.0",
4
4
  "description": "Cross-runtime video and animated-GIF helpers with browser-based ffmpeg.wasm encoding for Squisq documents",
5
5
  "license": "MIT",
6
6
  "author": "Bendyline",
@@ -48,7 +48,7 @@
48
48
  "typecheck": "tsc --noEmit"
49
49
  },
50
50
  "dependencies": {
51
- "@bendyline/squisq": "2.7.0",
51
+ "@bendyline/squisq": "2.8.0",
52
52
  "@ffmpeg/ffmpeg": "0.12.15",
53
53
  "@ffmpeg/util": "0.12.2"
54
54
  },