@shotstack/shotstack-canvas 1.6.3 → 1.6.5

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
  }
@@ -879,42 +932,42 @@ async function buildDrawOps(p) {
879
932
  const scale = p.font.size / upem;
880
933
  const numLines = p.lines.length;
881
934
  const lineHeightPx = p.font.size * p.style.lineHeight;
882
- const textOffsetY = borderWidth + padding.top;
883
935
  let blockY;
884
936
  switch (p.align.vertical) {
885
937
  case "top":
886
- blockY = p.font.size + textOffsetY;
938
+ blockY = p.font.size;
887
939
  break;
888
940
  case "bottom":
889
- blockY = p.textRect.height - (numLines - 1) * lineHeightPx + textOffsetY;
941
+ blockY = p.textRect.height - (numLines - 1) * lineHeightPx;
890
942
  break;
891
943
  case "middle":
892
944
  default:
893
945
  const capHeightRatio = 0.35;
894
946
  const visualOffset = p.font.size * capHeightRatio;
895
- blockY = (p.textRect.height - (numLines - 1) * lineHeightPx) / 2 + visualOffset + textOffsetY;
947
+ blockY = (p.textRect.height - (numLines - 1) * lineHeightPx) / 2 + visualOffset;
896
948
  break;
897
949
  }
950
+ blockY += borderWidth + padding.top;
898
951
  const fill = p.style.gradient ? gradientSpecFrom(p.style.gradient, 1) : { kind: "solid", color: p.font.color, opacity: p.font.opacity };
899
952
  const decoColor = p.style.gradient ? p.style.gradient.stops[p.style.gradient.stops.length - 1]?.color ?? p.font.color : p.font.color;
900
953
  const textOps = [];
901
954
  const highlighterOps = [];
902
955
  let gMinX = Infinity, gMinY = Infinity, gMaxX = -Infinity, gMaxY = -Infinity;
903
956
  for (const line of p.lines) {
904
- const textOffsetX = borderWidth + padding.left;
905
957
  let lineX;
906
958
  switch (p.align.horizontal) {
907
959
  case "left":
908
- lineX = textOffsetX;
960
+ lineX = 0;
909
961
  break;
910
962
  case "right":
911
- lineX = p.textRect.width - line.width + textOffsetX;
963
+ lineX = p.textRect.width - line.width;
912
964
  break;
913
965
  case "center":
914
966
  default:
915
- lineX = (p.textRect.width - line.width) / 2 + textOffsetX;
967
+ lineX = (p.textRect.width - line.width) / 2;
916
968
  break;
917
969
  }
970
+ lineX += borderWidth + padding.left;
918
971
  let xCursor = lineX;
919
972
  const lineIndex = p.lines.indexOf(line);
920
973
  const baselineY = blockY + lineIndex * lineHeightPx;
@@ -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
  }
@@ -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
  }
@@ -840,42 +893,42 @@ async function buildDrawOps(p) {
840
893
  const scale = p.font.size / upem;
841
894
  const numLines = p.lines.length;
842
895
  const lineHeightPx = p.font.size * p.style.lineHeight;
843
- const textOffsetY = borderWidth + padding.top;
844
896
  let blockY;
845
897
  switch (p.align.vertical) {
846
898
  case "top":
847
- blockY = p.font.size + textOffsetY;
899
+ blockY = p.font.size;
848
900
  break;
849
901
  case "bottom":
850
- blockY = p.textRect.height - (numLines - 1) * lineHeightPx + textOffsetY;
902
+ blockY = p.textRect.height - (numLines - 1) * lineHeightPx;
851
903
  break;
852
904
  case "middle":
853
905
  default:
854
906
  const capHeightRatio = 0.35;
855
907
  const visualOffset = p.font.size * capHeightRatio;
856
- blockY = (p.textRect.height - (numLines - 1) * lineHeightPx) / 2 + visualOffset + textOffsetY;
908
+ blockY = (p.textRect.height - (numLines - 1) * lineHeightPx) / 2 + visualOffset;
857
909
  break;
858
910
  }
911
+ blockY += borderWidth + padding.top;
859
912
  const fill = p.style.gradient ? gradientSpecFrom(p.style.gradient, 1) : { kind: "solid", color: p.font.color, opacity: p.font.opacity };
860
913
  const decoColor = p.style.gradient ? p.style.gradient.stops[p.style.gradient.stops.length - 1]?.color ?? p.font.color : p.font.color;
861
914
  const textOps = [];
862
915
  const highlighterOps = [];
863
916
  let gMinX = Infinity, gMinY = Infinity, gMaxX = -Infinity, gMaxY = -Infinity;
864
917
  for (const line of p.lines) {
865
- const textOffsetX = borderWidth + padding.left;
866
918
  let lineX;
867
919
  switch (p.align.horizontal) {
868
920
  case "left":
869
- lineX = textOffsetX;
921
+ lineX = 0;
870
922
  break;
871
923
  case "right":
872
- lineX = p.textRect.width - line.width + textOffsetX;
924
+ lineX = p.textRect.width - line.width;
873
925
  break;
874
926
  case "center":
875
927
  default:
876
- lineX = (p.textRect.width - line.width) / 2 + textOffsetX;
928
+ lineX = (p.textRect.width - line.width) / 2;
877
929
  break;
878
930
  }
931
+ lineX += borderWidth + padding.left;
879
932
  let xCursor = lineX;
880
933
  const lineIndex = p.lines.indexOf(line);
881
934
  const baselineY = blockY + lineIndex * lineHeightPx;
@@ -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
  }
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
 
@@ -845,42 +898,42 @@ async function buildDrawOps(p) {
845
898
  const scale = p.font.size / upem;
846
899
  const numLines = p.lines.length;
847
900
  const lineHeightPx = p.font.size * p.style.lineHeight;
848
- const textOffsetY = borderWidth + padding.top;
849
901
  let blockY;
850
902
  switch (p.align.vertical) {
851
903
  case "top":
852
- blockY = p.font.size + textOffsetY;
904
+ blockY = p.font.size;
853
905
  break;
854
906
  case "bottom":
855
- blockY = p.textRect.height - (numLines - 1) * lineHeightPx + textOffsetY;
907
+ blockY = p.textRect.height - (numLines - 1) * lineHeightPx;
856
908
  break;
857
909
  case "middle":
858
910
  default:
859
911
  const capHeightRatio = 0.35;
860
912
  const visualOffset = p.font.size * capHeightRatio;
861
- blockY = (p.textRect.height - (numLines - 1) * lineHeightPx) / 2 + visualOffset + textOffsetY;
913
+ blockY = (p.textRect.height - (numLines - 1) * lineHeightPx) / 2 + visualOffset;
862
914
  break;
863
915
  }
916
+ blockY += borderWidth + padding.top;
864
917
  const fill = p.style.gradient ? gradientSpecFrom(p.style.gradient, 1) : { kind: "solid", color: p.font.color, opacity: p.font.opacity };
865
918
  const decoColor = p.style.gradient ? p.style.gradient.stops[p.style.gradient.stops.length - 1]?.color ?? p.font.color : p.font.color;
866
919
  const textOps = [];
867
920
  const highlighterOps = [];
868
921
  let gMinX = Infinity, gMinY = Infinity, gMaxX = -Infinity, gMaxY = -Infinity;
869
922
  for (const line of p.lines) {
870
- const textOffsetX = borderWidth + padding.left;
871
923
  let lineX;
872
924
  switch (p.align.horizontal) {
873
925
  case "left":
874
- lineX = textOffsetX;
926
+ lineX = 0;
875
927
  break;
876
928
  case "right":
877
- lineX = p.textRect.width - line.width + textOffsetX;
929
+ lineX = p.textRect.width - line.width;
878
930
  break;
879
931
  case "center":
880
932
  default:
881
- lineX = (p.textRect.width - line.width) / 2 + textOffsetX;
933
+ lineX = (p.textRect.width - line.width) / 2;
882
934
  break;
883
935
  }
936
+ lineX += borderWidth + padding.left;
884
937
  let xCursor = lineX;
885
938
  const lineIndex = p.lines.indexOf(line);
886
939
  const baselineY = blockY + lineIndex * lineHeightPx;
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "@shotstack/shotstack-canvas",
3
- "version": "1.6.3",
3
+ "version": "1.6.5",
4
4
  "description": "Text layout & animation engine (HarfBuzz) for Node & Web - fully self-contained.",
5
5
  "type": "module",
6
6
  "main": "./dist/entry.node.cjs",