@shotstack/shotstack-canvas 1.6.4 → 1.6.6

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.
@@ -414,10 +414,63 @@ var FontRegistry = class _FontRegistry {
414
414
  wasmBaseURL;
415
415
  initPromise;
416
416
  emojiFallbackDesc;
417
+ static sharedInstance = null;
418
+ static refCount = 0;
419
+ static sharedInitPromise = null;
417
420
  static fallbackLoader;
418
421
  static setFallbackLoader(loader) {
419
422
  _FontRegistry.fallbackLoader = loader;
420
423
  }
424
+ static async getSharedInstance(wasmBaseURL) {
425
+ if (_FontRegistry.sharedInitPromise) {
426
+ const instance = await _FontRegistry.sharedInitPromise;
427
+ _FontRegistry.refCount++;
428
+ return instance;
429
+ }
430
+ if (_FontRegistry.sharedInstance) {
431
+ _FontRegistry.refCount++;
432
+ return _FontRegistry.sharedInstance;
433
+ }
434
+ _FontRegistry.sharedInitPromise = (async () => {
435
+ const instance = new _FontRegistry(wasmBaseURL);
436
+ await instance.init();
437
+ _FontRegistry.sharedInstance = instance;
438
+ _FontRegistry.refCount = 1;
439
+ return instance;
440
+ })();
441
+ try {
442
+ const instance = await _FontRegistry.sharedInitPromise;
443
+ return instance;
444
+ } finally {
445
+ _FontRegistry.sharedInitPromise = null;
446
+ }
447
+ }
448
+ static getRefCount() {
449
+ return _FontRegistry.refCount;
450
+ }
451
+ static hasSharedInstance() {
452
+ return _FontRegistry.sharedInstance !== null;
453
+ }
454
+ release() {
455
+ if (this !== _FontRegistry.sharedInstance) {
456
+ this.destroy();
457
+ return;
458
+ }
459
+ _FontRegistry.refCount--;
460
+ if (_FontRegistry.refCount <= 0) {
461
+ this.destroy();
462
+ _FontRegistry.sharedInstance = null;
463
+ _FontRegistry.refCount = 0;
464
+ }
465
+ }
466
+ static resetSharedInstance() {
467
+ if (_FontRegistry.sharedInstance) {
468
+ _FontRegistry.sharedInstance.destroy();
469
+ _FontRegistry.sharedInstance = null;
470
+ }
471
+ _FontRegistry.refCount = 0;
472
+ _FontRegistry.sharedInitPromise = null;
473
+ }
421
474
  setEmojiFallback(desc) {
422
475
  this.emojiFallbackDesc = desc;
423
476
  }
@@ -2386,16 +2439,16 @@ async function createTextEngine(opts = {}) {
2386
2439
  const pixelRatio = opts.pixelRatio ?? CANVAS_CONFIG.DEFAULTS.pixelRatio;
2387
2440
  const fps = opts.fps ?? 30;
2388
2441
  const wasmBaseURL = opts.wasmBaseURL;
2389
- const fonts = new FontRegistry(wasmBaseURL);
2390
- const layout = new LayoutEngine(fonts);
2391
- const videoGenerator = new VideoGenerator();
2442
+ let fonts;
2392
2443
  try {
2393
- await fonts.init();
2444
+ fonts = await FontRegistry.getSharedInstance(wasmBaseURL);
2394
2445
  } catch (err) {
2395
2446
  throw new Error(
2396
2447
  `Failed to initialize font registry: ${err instanceof Error ? err.message : String(err)}`
2397
2448
  );
2398
2449
  }
2450
+ const layout = new LayoutEngine(fonts);
2451
+ const videoGenerator = new VideoGenerator();
2399
2452
  async function ensureFonts(asset) {
2400
2453
  try {
2401
2454
  if (asset.customFonts) {
@@ -2628,7 +2681,7 @@ async function createTextEngine(opts = {}) {
2628
2681
  },
2629
2682
  destroy() {
2630
2683
  try {
2631
- fonts.destroy();
2684
+ fonts.release();
2632
2685
  } catch (err) {
2633
2686
  console.error(`Error during cleanup: ${err instanceof Error ? err.message : String(err)}`);
2634
2687
  }
@@ -1,3 +1,5 @@
1
+ import { components } from '@shotstack/schemas';
2
+
1
3
  declare const CANVAS_CONFIG: {
2
4
  DEFAULTS: {
3
5
  width: number;
@@ -22,6 +24,7 @@ declare const CANVAS_CONFIG: {
22
24
  ANIMATION_TYPES: readonly ["typewriter", "fadeIn", "slideIn", "shift", "ascend", "movingLetters"];
23
25
  };
24
26
 
27
+ type ShotstackRichTextAsset = components["schemas"]["RichTextAsset"];
25
28
  type RichTextValidated = Required<{
26
29
  type: "rich-text";
27
30
  text: string;
@@ -278,4 +281,4 @@ declare function createTextEngine(opts?: {
278
281
  destroy(): void;
279
282
  }>;
280
283
 
281
- export { type DrawOp, type EngineInit, type Glyph, type GradientSpec, type RGBA, type Renderer, type ShapedLine, type ValidAsset, createTextEngine, isGlyphFill, isShadowFill };
284
+ export { type DrawOp, type EngineInit, type Glyph, type GradientSpec, type RGBA, type Renderer, type ShapedLine, type ShotstackRichTextAsset, type ValidAsset, createTextEngine, isGlyphFill, isShadowFill };
@@ -1,3 +1,5 @@
1
+ import { components } from '@shotstack/schemas';
2
+
1
3
  declare const CANVAS_CONFIG: {
2
4
  DEFAULTS: {
3
5
  width: number;
@@ -22,6 +24,7 @@ declare const CANVAS_CONFIG: {
22
24
  ANIMATION_TYPES: readonly ["typewriter", "fadeIn", "slideIn", "shift", "ascend", "movingLetters"];
23
25
  };
24
26
 
27
+ type ShotstackRichTextAsset = components["schemas"]["RichTextAsset"];
25
28
  type RichTextValidated = Required<{
26
29
  type: "rich-text";
27
30
  text: string;
@@ -278,4 +281,4 @@ declare function createTextEngine(opts?: {
278
281
  destroy(): void;
279
282
  }>;
280
283
 
281
- export { type DrawOp, type EngineInit, type Glyph, type GradientSpec, type RGBA, type Renderer, type ShapedLine, type ValidAsset, createTextEngine, isGlyphFill, isShadowFill };
284
+ export { type DrawOp, type EngineInit, type Glyph, type GradientSpec, type RGBA, type Renderer, type ShapedLine, type ShotstackRichTextAsset, type ValidAsset, createTextEngine, isGlyphFill, isShadowFill };
@@ -375,10 +375,63 @@ var FontRegistry = class _FontRegistry {
375
375
  wasmBaseURL;
376
376
  initPromise;
377
377
  emojiFallbackDesc;
378
+ static sharedInstance = null;
379
+ static refCount = 0;
380
+ static sharedInitPromise = null;
378
381
  static fallbackLoader;
379
382
  static setFallbackLoader(loader) {
380
383
  _FontRegistry.fallbackLoader = loader;
381
384
  }
385
+ static async getSharedInstance(wasmBaseURL) {
386
+ if (_FontRegistry.sharedInitPromise) {
387
+ const instance = await _FontRegistry.sharedInitPromise;
388
+ _FontRegistry.refCount++;
389
+ return instance;
390
+ }
391
+ if (_FontRegistry.sharedInstance) {
392
+ _FontRegistry.refCount++;
393
+ return _FontRegistry.sharedInstance;
394
+ }
395
+ _FontRegistry.sharedInitPromise = (async () => {
396
+ const instance = new _FontRegistry(wasmBaseURL);
397
+ await instance.init();
398
+ _FontRegistry.sharedInstance = instance;
399
+ _FontRegistry.refCount = 1;
400
+ return instance;
401
+ })();
402
+ try {
403
+ const instance = await _FontRegistry.sharedInitPromise;
404
+ return instance;
405
+ } finally {
406
+ _FontRegistry.sharedInitPromise = null;
407
+ }
408
+ }
409
+ static getRefCount() {
410
+ return _FontRegistry.refCount;
411
+ }
412
+ static hasSharedInstance() {
413
+ return _FontRegistry.sharedInstance !== null;
414
+ }
415
+ release() {
416
+ if (this !== _FontRegistry.sharedInstance) {
417
+ this.destroy();
418
+ return;
419
+ }
420
+ _FontRegistry.refCount--;
421
+ if (_FontRegistry.refCount <= 0) {
422
+ this.destroy();
423
+ _FontRegistry.sharedInstance = null;
424
+ _FontRegistry.refCount = 0;
425
+ }
426
+ }
427
+ static resetSharedInstance() {
428
+ if (_FontRegistry.sharedInstance) {
429
+ _FontRegistry.sharedInstance.destroy();
430
+ _FontRegistry.sharedInstance = null;
431
+ }
432
+ _FontRegistry.refCount = 0;
433
+ _FontRegistry.sharedInitPromise = null;
434
+ }
382
435
  setEmojiFallback(desc) {
383
436
  this.emojiFallbackDesc = desc;
384
437
  }
@@ -2347,16 +2400,16 @@ async function createTextEngine(opts = {}) {
2347
2400
  const pixelRatio = opts.pixelRatio ?? CANVAS_CONFIG.DEFAULTS.pixelRatio;
2348
2401
  const fps = opts.fps ?? 30;
2349
2402
  const wasmBaseURL = opts.wasmBaseURL;
2350
- const fonts = new FontRegistry(wasmBaseURL);
2351
- const layout = new LayoutEngine(fonts);
2352
- const videoGenerator = new VideoGenerator();
2403
+ let fonts;
2353
2404
  try {
2354
- await fonts.init();
2405
+ fonts = await FontRegistry.getSharedInstance(wasmBaseURL);
2355
2406
  } catch (err) {
2356
2407
  throw new Error(
2357
2408
  `Failed to initialize font registry: ${err instanceof Error ? err.message : String(err)}`
2358
2409
  );
2359
2410
  }
2411
+ const layout = new LayoutEngine(fonts);
2412
+ const videoGenerator = new VideoGenerator();
2360
2413
  async function ensureFonts(asset) {
2361
2414
  try {
2362
2415
  if (asset.customFonts) {
@@ -2589,7 +2642,7 @@ async function createTextEngine(opts = {}) {
2589
2642
  },
2590
2643
  destroy() {
2591
2644
  try {
2592
- fonts.destroy();
2645
+ fonts.release();
2593
2646
  } catch (err) {
2594
2647
  console.error(`Error during cleanup: ${err instanceof Error ? err.message : String(err)}`);
2595
2648
  }
@@ -1,3 +1,5 @@
1
+ import { components } from '@shotstack/schemas';
2
+
1
3
  declare const CANVAS_CONFIG: {
2
4
  DEFAULTS: {
3
5
  width: number;
@@ -22,6 +24,7 @@ declare const CANVAS_CONFIG: {
22
24
  ANIMATION_TYPES: readonly ["typewriter", "fadeIn", "slideIn", "shift", "ascend", "movingLetters"];
23
25
  };
24
26
 
27
+ type ShotstackRichTextAsset = components["schemas"]["RichTextAsset"];
25
28
  type RichTextValidated = Required<{
26
29
  type: "rich-text";
27
30
  text: string;
@@ -255,4 +258,4 @@ declare function createTextEngine(opts?: {
255
258
  destroy(): void;
256
259
  }>;
257
260
 
258
- export { type DrawOp, type EngineInit, type Glyph, type GradientSpec, type RGBA, type Renderer, type ShapedLine, type ValidAsset, createTextEngine, isGlyphFill, isShadowFill };
261
+ export { type DrawOp, type EngineInit, type Glyph, type GradientSpec, type RGBA, type Renderer, type ShapedLine, type ShotstackRichTextAsset, type ValidAsset, createTextEngine, isGlyphFill, isShadowFill };
package/dist/entry.web.js CHANGED
@@ -385,6 +385,56 @@ var _FontRegistry = class _FontRegistry {
385
385
  static setFallbackLoader(loader) {
386
386
  _FontRegistry.fallbackLoader = loader;
387
387
  }
388
+ static async getSharedInstance(wasmBaseURL) {
389
+ if (_FontRegistry.sharedInitPromise) {
390
+ const instance = await _FontRegistry.sharedInitPromise;
391
+ _FontRegistry.refCount++;
392
+ return instance;
393
+ }
394
+ if (_FontRegistry.sharedInstance) {
395
+ _FontRegistry.refCount++;
396
+ return _FontRegistry.sharedInstance;
397
+ }
398
+ _FontRegistry.sharedInitPromise = (async () => {
399
+ const instance = new _FontRegistry(wasmBaseURL);
400
+ await instance.init();
401
+ _FontRegistry.sharedInstance = instance;
402
+ _FontRegistry.refCount = 1;
403
+ return instance;
404
+ })();
405
+ try {
406
+ const instance = await _FontRegistry.sharedInitPromise;
407
+ return instance;
408
+ } finally {
409
+ _FontRegistry.sharedInitPromise = null;
410
+ }
411
+ }
412
+ static getRefCount() {
413
+ return _FontRegistry.refCount;
414
+ }
415
+ static hasSharedInstance() {
416
+ return _FontRegistry.sharedInstance !== null;
417
+ }
418
+ release() {
419
+ if (this !== _FontRegistry.sharedInstance) {
420
+ this.destroy();
421
+ return;
422
+ }
423
+ _FontRegistry.refCount--;
424
+ if (_FontRegistry.refCount <= 0) {
425
+ this.destroy();
426
+ _FontRegistry.sharedInstance = null;
427
+ _FontRegistry.refCount = 0;
428
+ }
429
+ }
430
+ static resetSharedInstance() {
431
+ if (_FontRegistry.sharedInstance) {
432
+ _FontRegistry.sharedInstance.destroy();
433
+ _FontRegistry.sharedInstance = null;
434
+ }
435
+ _FontRegistry.refCount = 0;
436
+ _FontRegistry.sharedInitPromise = null;
437
+ }
388
438
  setEmojiFallback(desc) {
389
439
  this.emojiFallbackDesc = desc;
390
440
  }
@@ -597,6 +647,9 @@ var _FontRegistry = class _FontRegistry {
597
647
  }
598
648
  }
599
649
  };
650
+ __publicField(_FontRegistry, "sharedInstance", null);
651
+ __publicField(_FontRegistry, "refCount", 0);
652
+ __publicField(_FontRegistry, "sharedInitPromise", null);
600
653
  __publicField(_FontRegistry, "fallbackLoader");
601
654
  var FontRegistry = _FontRegistry;
602
655
 
@@ -2013,23 +2066,23 @@ async function createTextEngine(opts = {}) {
2013
2066
  const height = opts.height ?? CANVAS_CONFIG.DEFAULTS.height;
2014
2067
  const pixelRatio = opts.pixelRatio ?? CANVAS_CONFIG.DEFAULTS.pixelRatio;
2015
2068
  const wasmBaseURL = "https://shotstack-ingest-api-dev-sources.s3.ap-southeast-2.amazonaws.com/euo5r93oyr/zzz01k9h-yycyx-2x2y6-qx9bj-7n567b/source.wasm";
2016
- const fonts = new FontRegistry(wasmBaseURL);
2017
- const layout = new LayoutEngine(fonts);
2069
+ FontRegistry.setFallbackLoader(async (desc) => {
2070
+ const family = (desc.family ?? "Roboto").toLowerCase();
2071
+ const weight = `${desc.weight ?? "400"}`;
2072
+ if (family === "roboto" && weight === "400") {
2073
+ return fetchToArrayBuffer(DEFAULT_ROBOTO_URL);
2074
+ }
2075
+ return void 0;
2076
+ });
2077
+ let fonts;
2018
2078
  try {
2019
- FontRegistry.setFallbackLoader(async (desc) => {
2020
- const family = (desc.family ?? "Roboto").toLowerCase();
2021
- const weight = `${desc.weight ?? "400"}`;
2022
- if (family === "roboto" && weight === "400") {
2023
- return fetchToArrayBuffer(DEFAULT_ROBOTO_URL);
2024
- }
2025
- return void 0;
2026
- });
2027
- await fonts.init();
2079
+ fonts = await FontRegistry.getSharedInstance(wasmBaseURL);
2028
2080
  } catch (err) {
2029
2081
  throw new Error(
2030
2082
  `Failed to initialize font registry: ${err instanceof Error ? err.message : String(err)}`
2031
2083
  );
2032
2084
  }
2085
+ const layout = new LayoutEngine(fonts);
2033
2086
  async function ensureFonts(asset) {
2034
2087
  try {
2035
2088
  if (asset.customFonts) {
@@ -2067,9 +2120,7 @@ async function createTextEngine(opts = {}) {
2067
2120
  weight: "400"
2068
2121
  });
2069
2122
  } else {
2070
- throw new Error(
2071
- `Font not registered for ${desc.family}__${desc.weight}`
2072
- );
2123
+ throw new Error(`Font not registered for ${desc.family}__${desc.weight}`);
2073
2124
  }
2074
2125
  }
2075
2126
  };
@@ -2149,7 +2200,12 @@ async function createTextEngine(opts = {}) {
2149
2200
  emojiAvailable = true;
2150
2201
  } catch {
2151
2202
  }
2152
- const padding2 = asset.padding ? typeof asset.padding === "number" ? { top: asset.padding, right: asset.padding, bottom: asset.padding, left: asset.padding } : asset.padding : { top: 0, right: 0, bottom: 0, left: 0 };
2203
+ const padding2 = asset.padding ? typeof asset.padding === "number" ? {
2204
+ top: asset.padding,
2205
+ right: asset.padding,
2206
+ bottom: asset.padding,
2207
+ left: asset.padding
2208
+ } : asset.padding : { top: 0, right: 0, bottom: 0, left: 0 };
2153
2209
  lines = await layout.layout({
2154
2210
  text: asset.text,
2155
2211
  width: (asset.width ?? width) - padding2.left - padding2.right,
@@ -2165,7 +2221,12 @@ async function createTextEngine(opts = {}) {
2165
2221
  `Failed to layout text: ${err instanceof Error ? err.message : String(err)}`
2166
2222
  );
2167
2223
  }
2168
- const padding = asset.padding ? typeof asset.padding === "number" ? { top: asset.padding, right: asset.padding, bottom: asset.padding, left: asset.padding } : asset.padding : { top: 0, right: 0, bottom: 0, left: 0 };
2224
+ const padding = asset.padding ? typeof asset.padding === "number" ? {
2225
+ top: asset.padding,
2226
+ right: asset.padding,
2227
+ bottom: asset.padding,
2228
+ left: asset.padding
2229
+ } : asset.padding : { top: 0, right: 0, bottom: 0, left: 0 };
2169
2230
  const borderWidth = asset.border?.width ?? 0;
2170
2231
  const canvasW = (asset.width ?? width) + borderWidth * 2;
2171
2232
  const canvasH = (asset.height ?? height) + borderWidth * 2;
@@ -2252,7 +2313,7 @@ async function createTextEngine(opts = {}) {
2252
2313
  },
2253
2314
  destroy() {
2254
2315
  try {
2255
- fonts.destroy();
2316
+ fonts.release();
2256
2317
  } catch (err) {
2257
2318
  console.error(`Error during cleanup: ${err instanceof Error ? err.message : String(err)}`);
2258
2319
  }
package/package.json CHANGED
@@ -1,62 +1,63 @@
1
- {
2
- "name": "@shotstack/shotstack-canvas",
3
- "version": "1.6.4",
4
- "description": "Text layout & animation engine (HarfBuzz) for Node & Web - fully self-contained.",
5
- "type": "module",
6
- "main": "./dist/entry.node.cjs",
7
- "module": "./dist/entry.node.js",
8
- "browser": "./dist/entry.web.js",
9
- "types": "./dist/entry.node.d.ts",
10
- "exports": {
11
- ".": {
12
- "node": {
13
- "import": "./dist/entry.node.js",
14
- "require": "./dist/entry.node.cjs"
15
- },
16
- "browser": "./dist/entry.web.js",
17
- "default": "./dist/entry.web.js"
18
- }
19
- },
20
- "files": [
21
- "dist/**",
22
- "scripts/postinstall.js",
23
- "README.md",
24
- "LICENSE"
25
- ],
26
- "scripts": {
27
- "dev": "tsup --watch",
28
- "build": "tsup",
29
- "postinstall": "node scripts/postinstall.js",
30
- "vendor:harfbuzz": "node scripts/vendor-harfbuzz.js",
31
- "example:node": "node examples/node-example.mjs",
32
- "example:video": "node examples/node-video.mjs",
33
- "example:web": "vite dev examples/web-example",
34
- "prepublishOnly": "npm run build"
35
- },
36
- "publishConfig": {
37
- "access": "public",
38
- "registry": "https://registry.npmjs.org/"
39
- },
40
- "engines": {
41
- "node": ">=18"
42
- },
43
- "sideEffects": false,
44
- "dependencies": {
45
- "canvas": "npm:@napi-rs/canvas@^0.1.54",
46
- "ffmpeg-static": "^5.2.0",
47
- "fontkit": "^2.0.4",
48
- "harfbuzzjs": "0.4.12",
49
- "joi": "^17.13.3",
50
- "opentype.js": "^1.3.4"
51
- },
52
- "devDependencies": {
53
- "@types/fluent-ffmpeg": "2.1.27",
54
- "@types/node": "^20.14.10",
55
- "fluent-ffmpeg": "^2.1.3",
56
- "tsup": "^8.2.3",
57
- "typescript": "^5.5.3",
58
- "vite": "^5.3.3",
59
- "vite-plugin-top-level-await": "1.6.0",
60
- "vite-plugin-wasm": "3.5.0"
61
- }
62
- }
1
+ {
2
+ "name": "@shotstack/shotstack-canvas",
3
+ "version": "1.6.6",
4
+ "description": "Text layout & animation engine (HarfBuzz) for Node & Web - fully self-contained.",
5
+ "type": "module",
6
+ "main": "./dist/entry.node.cjs",
7
+ "module": "./dist/entry.node.js",
8
+ "browser": "./dist/entry.web.js",
9
+ "types": "./dist/entry.node.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "node": {
13
+ "import": "./dist/entry.node.js",
14
+ "require": "./dist/entry.node.cjs"
15
+ },
16
+ "browser": "./dist/entry.web.js",
17
+ "default": "./dist/entry.web.js"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist/**",
22
+ "scripts/postinstall.js",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "scripts": {
27
+ "dev": "tsup --watch",
28
+ "build": "tsup",
29
+ "postinstall": "node scripts/postinstall.js",
30
+ "vendor:harfbuzz": "node scripts/vendor-harfbuzz.js",
31
+ "example:node": "node examples/node-example.mjs",
32
+ "example:video": "node examples/node-video.mjs",
33
+ "example:web": "vite dev examples/web-example",
34
+ "prepublishOnly": "npm run build"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public",
38
+ "registry": "https://registry.npmjs.org/"
39
+ },
40
+ "engines": {
41
+ "node": ">=18"
42
+ },
43
+ "sideEffects": false,
44
+ "dependencies": {
45
+ "@shotstack/schemas": "^1.0.1",
46
+ "canvas": "npm:@napi-rs/canvas@^0.1.54",
47
+ "ffmpeg-static": "^5.2.0",
48
+ "fontkit": "^2.0.4",
49
+ "harfbuzzjs": "0.4.12",
50
+ "joi": "^17.13.3",
51
+ "opentype.js": "^1.3.4"
52
+ },
53
+ "devDependencies": {
54
+ "@types/fluent-ffmpeg": "2.1.27",
55
+ "@types/node": "^20.14.10",
56
+ "fluent-ffmpeg": "^2.1.3",
57
+ "tsup": "^8.2.3",
58
+ "typescript": "^5.5.3",
59
+ "vite": "^5.3.3",
60
+ "vite-plugin-top-level-await": "1.6.0",
61
+ "vite-plugin-wasm": "3.5.0"
62
+ }
63
+ }