@shotstack/shotstack-canvas 1.3.7 → 1.3.9

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.
@@ -370,10 +370,14 @@ var FontRegistry = class _FontRegistry {
370
370
  fontkitFonts = /* @__PURE__ */ new Map();
371
371
  wasmBaseURL;
372
372
  initPromise;
373
+ emojiFallbackDesc;
373
374
  static fallbackLoader;
374
375
  static setFallbackLoader(loader) {
375
376
  _FontRegistry.fallbackLoader = loader;
376
377
  }
378
+ setEmojiFallback(desc) {
379
+ this.emojiFallbackDesc = desc;
380
+ }
377
381
  constructor(wasmBaseURL) {
378
382
  this.wasmBaseURL = wasmBaseURL;
379
383
  }
@@ -531,7 +535,8 @@ var FontRegistry = class _FontRegistry {
531
535
  if (fkFont) {
532
536
  const glyph = fkFont.getGlyph(glyphId);
533
537
  if (glyph && glyph.path) {
534
- return glyph.path.toSVG();
538
+ const path2 = glyph.path.toSVG();
539
+ return path2 && path2 !== "" ? path2 : "M 0 0";
535
540
  }
536
541
  }
537
542
  const font = await this.getFont(desc);
@@ -587,6 +592,18 @@ var FontRegistry = class _FontRegistry {
587
592
  };
588
593
 
589
594
  // src/core/layout.ts
595
+ function isEmoji(char) {
596
+ const code = char.codePointAt(0);
597
+ if (!code) return false;
598
+ return code >= 127744 && code <= 129535 || // Emoticons, symbols, pictographs
599
+ code >= 9728 && code <= 9983 || // Miscellaneous symbols
600
+ code >= 9984 && code <= 10175 || // Dingbats
601
+ code >= 65024 && code <= 65039 || // Variation selectors
602
+ code >= 128512 && code <= 128591 || // Emoticons
603
+ code >= 128640 && code <= 128767 || // Transport and map symbols
604
+ code >= 129280 && code <= 129535 || // Supplemental symbols and pictographs
605
+ code >= 129648 && code <= 129791;
606
+ }
590
607
  var LayoutEngine = class {
591
608
  constructor(fonts) {
592
609
  this.fonts = fonts;
@@ -634,14 +651,43 @@ var LayoutEngine = class {
634
651
  }
635
652
  async layout(params) {
636
653
  try {
637
- const { textTransform, desc, fontSize, letterSpacing, width } = params;
654
+ const { textTransform, desc, fontSize, letterSpacing, width, emojiFallback } = params;
638
655
  const input = this.transformText(params.text, textTransform);
639
656
  if (!input || input.length === 0) {
640
657
  return [];
641
658
  }
642
659
  let shaped;
643
660
  try {
644
- shaped = await this.shapeFull(input, desc);
661
+ if (!emojiFallback) {
662
+ shaped = await this.shapeFull(input, desc);
663
+ } else {
664
+ const chars = Array.from(input);
665
+ const runs = [];
666
+ let currentRun = { text: "", startIndex: 0, isEmoji: false };
667
+ for (let i = 0; i < chars.length; i++) {
668
+ const char = chars[i];
669
+ const charIsEmoji = isEmoji(char);
670
+ if (i === 0) {
671
+ currentRun = { text: char, startIndex: 0, isEmoji: charIsEmoji };
672
+ } else if (currentRun.isEmoji === charIsEmoji) {
673
+ currentRun.text += char;
674
+ } else {
675
+ runs.push(currentRun);
676
+ currentRun = { text: char, startIndex: currentRun.startIndex + currentRun.text.length, isEmoji: charIsEmoji };
677
+ }
678
+ }
679
+ if (currentRun.text) runs.push(currentRun);
680
+ shaped = [];
681
+ for (const run of runs) {
682
+ const runFont = run.isEmoji ? emojiFallback : desc;
683
+ const runShaped = await this.shapeFull(run.text, runFont);
684
+ for (const glyph of runShaped) {
685
+ glyph.cl += run.startIndex;
686
+ glyph.fontDesc = runFont;
687
+ }
688
+ shaped.push(...runShaped);
689
+ }
690
+ }
645
691
  } catch (err) {
646
692
  throw new Error(`Text shaping failed: ${err instanceof Error ? err.message : String(err)}`);
647
693
  }
@@ -667,7 +713,9 @@ var LayoutEngine = class {
667
713
  xOffset: g.dx * scale,
668
714
  yOffset: -g.dy * scale,
669
715
  cluster: g.cl,
670
- char
716
+ char,
717
+ fontDesc: g.fontDesc
718
+ // Preserve font descriptor
671
719
  };
672
720
  });
673
721
  const lines = [];
@@ -777,18 +825,21 @@ async function buildDrawOps(p) {
777
825
  if (p.lines.length === 0) return ops;
778
826
  const upem = Math.max(1, await p.getUnitsPerEm());
779
827
  const scale = p.font.size / upem;
780
- const blockHeight = p.lines[p.lines.length - 1].y;
828
+ const numLines = p.lines.length;
829
+ const lineHeightPx = p.font.size * p.style.lineHeight;
781
830
  let blockY;
782
831
  switch (p.align.vertical) {
783
832
  case "top":
784
833
  blockY = p.font.size;
785
834
  break;
786
835
  case "bottom":
787
- blockY = p.textRect.height - blockHeight + p.font.size;
836
+ blockY = p.textRect.height - (numLines - 1) * lineHeightPx;
788
837
  break;
789
838
  case "middle":
790
839
  default:
791
- blockY = (p.textRect.height - blockHeight) / 2 + p.font.size;
840
+ const capHeightRatio = 0.35;
841
+ const visualOffset = p.font.size * capHeightRatio;
842
+ blockY = (p.textRect.height - (numLines - 1) * lineHeightPx) / 2 + visualOffset;
792
843
  break;
793
844
  }
794
845
  const fill = p.style.gradient ? gradientSpecFrom(p.style.gradient, 1) : { kind: "solid", color: p.font.color, opacity: p.font.opacity };
@@ -809,9 +860,10 @@ async function buildDrawOps(p) {
809
860
  break;
810
861
  }
811
862
  let xCursor = lineX;
812
- const baselineY = blockY + line.y - p.font.size;
863
+ const lineIndex = p.lines.indexOf(line);
864
+ const baselineY = blockY + lineIndex * lineHeightPx;
813
865
  for (const glyph of line.glyphs) {
814
- const path = await p.glyphPathProvider(glyph.id);
866
+ const path = await p.glyphPathProvider(glyph.id, glyph.fontDesc);
815
867
  if (!path || path === "M 0 0") {
816
868
  xCursor += glyph.xAdvance;
817
869
  continue;
@@ -2075,6 +2127,13 @@ async function createTextEngine(opts = {}) {
2075
2127
  const desc = { family: main.family, weight: `${main.weight}`, style: main.style };
2076
2128
  let lines;
2077
2129
  try {
2130
+ const emojiDesc = { family: "NotoEmoji", weight: "400", style: "normal" };
2131
+ let emojiAvailable = false;
2132
+ try {
2133
+ await fonts.getFace(emojiDesc);
2134
+ emojiAvailable = true;
2135
+ } catch {
2136
+ }
2078
2137
  lines = await layout.layout({
2079
2138
  text: asset.text,
2080
2139
  width: asset.width ?? width,
@@ -2082,7 +2141,8 @@ async function createTextEngine(opts = {}) {
2082
2141
  fontSize: main.size,
2083
2142
  lineHeight: asset.style?.lineHeight ?? 1.2,
2084
2143
  desc,
2085
- textTransform: asset.style?.textTransform ?? "none"
2144
+ textTransform: asset.style?.textTransform ?? "none",
2145
+ emojiFallback: emojiAvailable ? emojiDesc : void 0
2086
2146
  });
2087
2147
  } catch (err) {
2088
2148
  throw new Error(
@@ -2124,8 +2184,8 @@ async function createTextEngine(opts = {}) {
2124
2184
  vertical: asset.align?.vertical ?? "middle"
2125
2185
  },
2126
2186
  background: asset.background,
2127
- glyphPathProvider: (gid) => fonts.glyphPath(desc, gid),
2128
- getUnitsPerEm: () => fonts.getUnitsPerEm(desc)
2187
+ glyphPathProvider: (gid, fontDesc) => fonts.glyphPath(fontDesc || desc, gid),
2188
+ getUnitsPerEm: (fontDesc) => fonts.getUnitsPerEm(fontDesc || desc)
2129
2189
  });
2130
2190
  } catch (err) {
2131
2191
  throw new Error(
@@ -121,6 +121,11 @@ type Glyph = {
121
121
  yOffset: number;
122
122
  cluster: number;
123
123
  char?: string;
124
+ fontDesc?: {
125
+ family: string;
126
+ weight?: string | number;
127
+ style?: string;
128
+ };
124
129
  };
125
130
  type ShapedLine = {
126
131
  glyphs: Glyph[];
@@ -121,6 +121,11 @@ type Glyph = {
121
121
  yOffset: number;
122
122
  cluster: number;
123
123
  char?: string;
124
+ fontDesc?: {
125
+ family: string;
126
+ weight?: string | number;
127
+ style?: string;
128
+ };
124
129
  };
125
130
  type ShapedLine = {
126
131
  glyphs: Glyph[];
@@ -331,10 +331,14 @@ var FontRegistry = class _FontRegistry {
331
331
  fontkitFonts = /* @__PURE__ */ new Map();
332
332
  wasmBaseURL;
333
333
  initPromise;
334
+ emojiFallbackDesc;
334
335
  static fallbackLoader;
335
336
  static setFallbackLoader(loader) {
336
337
  _FontRegistry.fallbackLoader = loader;
337
338
  }
339
+ setEmojiFallback(desc) {
340
+ this.emojiFallbackDesc = desc;
341
+ }
338
342
  constructor(wasmBaseURL) {
339
343
  this.wasmBaseURL = wasmBaseURL;
340
344
  }
@@ -492,7 +496,8 @@ var FontRegistry = class _FontRegistry {
492
496
  if (fkFont) {
493
497
  const glyph = fkFont.getGlyph(glyphId);
494
498
  if (glyph && glyph.path) {
495
- return glyph.path.toSVG();
499
+ const path2 = glyph.path.toSVG();
500
+ return path2 && path2 !== "" ? path2 : "M 0 0";
496
501
  }
497
502
  }
498
503
  const font = await this.getFont(desc);
@@ -548,6 +553,18 @@ var FontRegistry = class _FontRegistry {
548
553
  };
549
554
 
550
555
  // src/core/layout.ts
556
+ function isEmoji(char) {
557
+ const code = char.codePointAt(0);
558
+ if (!code) return false;
559
+ return code >= 127744 && code <= 129535 || // Emoticons, symbols, pictographs
560
+ code >= 9728 && code <= 9983 || // Miscellaneous symbols
561
+ code >= 9984 && code <= 10175 || // Dingbats
562
+ code >= 65024 && code <= 65039 || // Variation selectors
563
+ code >= 128512 && code <= 128591 || // Emoticons
564
+ code >= 128640 && code <= 128767 || // Transport and map symbols
565
+ code >= 129280 && code <= 129535 || // Supplemental symbols and pictographs
566
+ code >= 129648 && code <= 129791;
567
+ }
551
568
  var LayoutEngine = class {
552
569
  constructor(fonts) {
553
570
  this.fonts = fonts;
@@ -595,14 +612,43 @@ var LayoutEngine = class {
595
612
  }
596
613
  async layout(params) {
597
614
  try {
598
- const { textTransform, desc, fontSize, letterSpacing, width } = params;
615
+ const { textTransform, desc, fontSize, letterSpacing, width, emojiFallback } = params;
599
616
  const input = this.transformText(params.text, textTransform);
600
617
  if (!input || input.length === 0) {
601
618
  return [];
602
619
  }
603
620
  let shaped;
604
621
  try {
605
- shaped = await this.shapeFull(input, desc);
622
+ if (!emojiFallback) {
623
+ shaped = await this.shapeFull(input, desc);
624
+ } else {
625
+ const chars = Array.from(input);
626
+ const runs = [];
627
+ let currentRun = { text: "", startIndex: 0, isEmoji: false };
628
+ for (let i = 0; i < chars.length; i++) {
629
+ const char = chars[i];
630
+ const charIsEmoji = isEmoji(char);
631
+ if (i === 0) {
632
+ currentRun = { text: char, startIndex: 0, isEmoji: charIsEmoji };
633
+ } else if (currentRun.isEmoji === charIsEmoji) {
634
+ currentRun.text += char;
635
+ } else {
636
+ runs.push(currentRun);
637
+ currentRun = { text: char, startIndex: currentRun.startIndex + currentRun.text.length, isEmoji: charIsEmoji };
638
+ }
639
+ }
640
+ if (currentRun.text) runs.push(currentRun);
641
+ shaped = [];
642
+ for (const run of runs) {
643
+ const runFont = run.isEmoji ? emojiFallback : desc;
644
+ const runShaped = await this.shapeFull(run.text, runFont);
645
+ for (const glyph of runShaped) {
646
+ glyph.cl += run.startIndex;
647
+ glyph.fontDesc = runFont;
648
+ }
649
+ shaped.push(...runShaped);
650
+ }
651
+ }
606
652
  } catch (err) {
607
653
  throw new Error(`Text shaping failed: ${err instanceof Error ? err.message : String(err)}`);
608
654
  }
@@ -628,7 +674,9 @@ var LayoutEngine = class {
628
674
  xOffset: g.dx * scale,
629
675
  yOffset: -g.dy * scale,
630
676
  cluster: g.cl,
631
- char
677
+ char,
678
+ fontDesc: g.fontDesc
679
+ // Preserve font descriptor
632
680
  };
633
681
  });
634
682
  const lines = [];
@@ -738,18 +786,21 @@ async function buildDrawOps(p) {
738
786
  if (p.lines.length === 0) return ops;
739
787
  const upem = Math.max(1, await p.getUnitsPerEm());
740
788
  const scale = p.font.size / upem;
741
- const blockHeight = p.lines[p.lines.length - 1].y;
789
+ const numLines = p.lines.length;
790
+ const lineHeightPx = p.font.size * p.style.lineHeight;
742
791
  let blockY;
743
792
  switch (p.align.vertical) {
744
793
  case "top":
745
794
  blockY = p.font.size;
746
795
  break;
747
796
  case "bottom":
748
- blockY = p.textRect.height - blockHeight + p.font.size;
797
+ blockY = p.textRect.height - (numLines - 1) * lineHeightPx;
749
798
  break;
750
799
  case "middle":
751
800
  default:
752
- blockY = (p.textRect.height - blockHeight) / 2 + p.font.size;
801
+ const capHeightRatio = 0.35;
802
+ const visualOffset = p.font.size * capHeightRatio;
803
+ blockY = (p.textRect.height - (numLines - 1) * lineHeightPx) / 2 + visualOffset;
753
804
  break;
754
805
  }
755
806
  const fill = p.style.gradient ? gradientSpecFrom(p.style.gradient, 1) : { kind: "solid", color: p.font.color, opacity: p.font.opacity };
@@ -770,9 +821,10 @@ async function buildDrawOps(p) {
770
821
  break;
771
822
  }
772
823
  let xCursor = lineX;
773
- const baselineY = blockY + line.y - p.font.size;
824
+ const lineIndex = p.lines.indexOf(line);
825
+ const baselineY = blockY + lineIndex * lineHeightPx;
774
826
  for (const glyph of line.glyphs) {
775
- const path = await p.glyphPathProvider(glyph.id);
827
+ const path = await p.glyphPathProvider(glyph.id, glyph.fontDesc);
776
828
  if (!path || path === "M 0 0") {
777
829
  xCursor += glyph.xAdvance;
778
830
  continue;
@@ -2036,6 +2088,13 @@ async function createTextEngine(opts = {}) {
2036
2088
  const desc = { family: main.family, weight: `${main.weight}`, style: main.style };
2037
2089
  let lines;
2038
2090
  try {
2091
+ const emojiDesc = { family: "NotoEmoji", weight: "400", style: "normal" };
2092
+ let emojiAvailable = false;
2093
+ try {
2094
+ await fonts.getFace(emojiDesc);
2095
+ emojiAvailable = true;
2096
+ } catch {
2097
+ }
2039
2098
  lines = await layout.layout({
2040
2099
  text: asset.text,
2041
2100
  width: asset.width ?? width,
@@ -2043,7 +2102,8 @@ async function createTextEngine(opts = {}) {
2043
2102
  fontSize: main.size,
2044
2103
  lineHeight: asset.style?.lineHeight ?? 1.2,
2045
2104
  desc,
2046
- textTransform: asset.style?.textTransform ?? "none"
2105
+ textTransform: asset.style?.textTransform ?? "none",
2106
+ emojiFallback: emojiAvailable ? emojiDesc : void 0
2047
2107
  });
2048
2108
  } catch (err) {
2049
2109
  throw new Error(
@@ -2085,8 +2145,8 @@ async function createTextEngine(opts = {}) {
2085
2145
  vertical: asset.align?.vertical ?? "middle"
2086
2146
  },
2087
2147
  background: asset.background,
2088
- glyphPathProvider: (gid) => fonts.glyphPath(desc, gid),
2089
- getUnitsPerEm: () => fonts.getUnitsPerEm(desc)
2148
+ glyphPathProvider: (gid, fontDesc) => fonts.glyphPath(fontDesc || desc, gid),
2149
+ getUnitsPerEm: (fontDesc) => fonts.getUnitsPerEm(fontDesc || desc)
2090
2150
  });
2091
2151
  } catch (err) {
2092
2152
  throw new Error(
@@ -121,6 +121,11 @@ type Glyph = {
121
121
  yOffset: number;
122
122
  cluster: number;
123
123
  char?: string;
124
+ fontDesc?: {
125
+ family: string;
126
+ weight?: string | number;
127
+ style?: string;
128
+ };
124
129
  };
125
130
  type ShapedLine = {
126
131
  glyphs: Glyph[];
package/dist/entry.web.js CHANGED
@@ -336,11 +336,15 @@ var _FontRegistry = class _FontRegistry {
336
336
  __publicField(this, "fontkitFonts", /* @__PURE__ */ new Map());
337
337
  __publicField(this, "wasmBaseURL");
338
338
  __publicField(this, "initPromise");
339
+ __publicField(this, "emojiFallbackDesc");
339
340
  this.wasmBaseURL = wasmBaseURL;
340
341
  }
341
342
  static setFallbackLoader(loader) {
342
343
  _FontRegistry.fallbackLoader = loader;
343
344
  }
345
+ setEmojiFallback(desc) {
346
+ this.emojiFallbackDesc = desc;
347
+ }
344
348
  async init() {
345
349
  if (this.initPromise) {
346
350
  await this.initPromise;
@@ -495,7 +499,8 @@ var _FontRegistry = class _FontRegistry {
495
499
  if (fkFont) {
496
500
  const glyph = fkFont.getGlyph(glyphId);
497
501
  if (glyph && glyph.path) {
498
- return glyph.path.toSVG();
502
+ const path2 = glyph.path.toSVG();
503
+ return path2 && path2 !== "" ? path2 : "M 0 0";
499
504
  }
500
505
  }
501
506
  const font = await this.getFont(desc);
@@ -553,6 +558,18 @@ __publicField(_FontRegistry, "fallbackLoader");
553
558
  var FontRegistry = _FontRegistry;
554
559
 
555
560
  // src/core/layout.ts
561
+ function isEmoji(char) {
562
+ const code = char.codePointAt(0);
563
+ if (!code) return false;
564
+ return code >= 127744 && code <= 129535 || // Emoticons, symbols, pictographs
565
+ code >= 9728 && code <= 9983 || // Miscellaneous symbols
566
+ code >= 9984 && code <= 10175 || // Dingbats
567
+ code >= 65024 && code <= 65039 || // Variation selectors
568
+ code >= 128512 && code <= 128591 || // Emoticons
569
+ code >= 128640 && code <= 128767 || // Transport and map symbols
570
+ code >= 129280 && code <= 129535 || // Supplemental symbols and pictographs
571
+ code >= 129648 && code <= 129791;
572
+ }
556
573
  var LayoutEngine = class {
557
574
  constructor(fonts) {
558
575
  this.fonts = fonts;
@@ -600,14 +617,43 @@ var LayoutEngine = class {
600
617
  }
601
618
  async layout(params) {
602
619
  try {
603
- const { textTransform, desc, fontSize, letterSpacing, width } = params;
620
+ const { textTransform, desc, fontSize, letterSpacing, width, emojiFallback } = params;
604
621
  const input = this.transformText(params.text, textTransform);
605
622
  if (!input || input.length === 0) {
606
623
  return [];
607
624
  }
608
625
  let shaped;
609
626
  try {
610
- shaped = await this.shapeFull(input, desc);
627
+ if (!emojiFallback) {
628
+ shaped = await this.shapeFull(input, desc);
629
+ } else {
630
+ const chars = Array.from(input);
631
+ const runs = [];
632
+ let currentRun = { text: "", startIndex: 0, isEmoji: false };
633
+ for (let i = 0; i < chars.length; i++) {
634
+ const char = chars[i];
635
+ const charIsEmoji = isEmoji(char);
636
+ if (i === 0) {
637
+ currentRun = { text: char, startIndex: 0, isEmoji: charIsEmoji };
638
+ } else if (currentRun.isEmoji === charIsEmoji) {
639
+ currentRun.text += char;
640
+ } else {
641
+ runs.push(currentRun);
642
+ currentRun = { text: char, startIndex: currentRun.startIndex + currentRun.text.length, isEmoji: charIsEmoji };
643
+ }
644
+ }
645
+ if (currentRun.text) runs.push(currentRun);
646
+ shaped = [];
647
+ for (const run of runs) {
648
+ const runFont = run.isEmoji ? emojiFallback : desc;
649
+ const runShaped = await this.shapeFull(run.text, runFont);
650
+ for (const glyph of runShaped) {
651
+ glyph.cl += run.startIndex;
652
+ glyph.fontDesc = runFont;
653
+ }
654
+ shaped.push(...runShaped);
655
+ }
656
+ }
611
657
  } catch (err) {
612
658
  throw new Error(`Text shaping failed: ${err instanceof Error ? err.message : String(err)}`);
613
659
  }
@@ -633,7 +679,9 @@ var LayoutEngine = class {
633
679
  xOffset: g.dx * scale,
634
680
  yOffset: -g.dy * scale,
635
681
  cluster: g.cl,
636
- char
682
+ char,
683
+ fontDesc: g.fontDesc
684
+ // Preserve font descriptor
637
685
  };
638
686
  });
639
687
  const lines = [];
@@ -743,18 +791,21 @@ async function buildDrawOps(p) {
743
791
  if (p.lines.length === 0) return ops;
744
792
  const upem = Math.max(1, await p.getUnitsPerEm());
745
793
  const scale = p.font.size / upem;
746
- const blockHeight = p.lines[p.lines.length - 1].y;
794
+ const numLines = p.lines.length;
795
+ const lineHeightPx = p.font.size * p.style.lineHeight;
747
796
  let blockY;
748
797
  switch (p.align.vertical) {
749
798
  case "top":
750
799
  blockY = p.font.size;
751
800
  break;
752
801
  case "bottom":
753
- blockY = p.textRect.height - blockHeight + p.font.size;
802
+ blockY = p.textRect.height - (numLines - 1) * lineHeightPx;
754
803
  break;
755
804
  case "middle":
756
805
  default:
757
- blockY = (p.textRect.height - blockHeight) / 2 + p.font.size;
806
+ const capHeightRatio = 0.35;
807
+ const visualOffset = p.font.size * capHeightRatio;
808
+ blockY = (p.textRect.height - (numLines - 1) * lineHeightPx) / 2 + visualOffset;
758
809
  break;
759
810
  }
760
811
  const fill = p.style.gradient ? gradientSpecFrom(p.style.gradient, 1) : { kind: "solid", color: p.font.color, opacity: p.font.opacity };
@@ -775,9 +826,10 @@ async function buildDrawOps(p) {
775
826
  break;
776
827
  }
777
828
  let xCursor = lineX;
778
- const baselineY = blockY + line.y - p.font.size;
829
+ const lineIndex = p.lines.indexOf(line);
830
+ const baselineY = blockY + lineIndex * lineHeightPx;
779
831
  for (const glyph of line.glyphs) {
780
- const path = await p.glyphPathProvider(glyph.id);
832
+ const path = await p.glyphPathProvider(glyph.id, glyph.fontDesc);
781
833
  if (!path || path === "M 0 0") {
782
834
  xCursor += glyph.xAdvance;
783
835
  continue;
@@ -1757,6 +1809,13 @@ async function createTextEngine(opts = {}) {
1757
1809
  const desc = { family: main.family, weight: `${main.weight}`, style: main.style };
1758
1810
  let lines;
1759
1811
  try {
1812
+ const emojiDesc = { family: "NotoEmoji", weight: "400", style: "normal" };
1813
+ let emojiAvailable = false;
1814
+ try {
1815
+ await fonts.getFace(emojiDesc);
1816
+ emojiAvailable = true;
1817
+ } catch {
1818
+ }
1760
1819
  lines = await layout.layout({
1761
1820
  text: asset.text,
1762
1821
  width: asset.width ?? width,
@@ -1764,7 +1823,8 @@ async function createTextEngine(opts = {}) {
1764
1823
  fontSize: main.size,
1765
1824
  lineHeight: asset.style?.lineHeight ?? 1.2,
1766
1825
  desc,
1767
- textTransform: asset.style?.textTransform ?? "none"
1826
+ textTransform: asset.style?.textTransform ?? "none",
1827
+ emojiFallback: emojiAvailable ? emojiDesc : void 0
1768
1828
  });
1769
1829
  } catch (err) {
1770
1830
  throw new Error(
@@ -1806,8 +1866,8 @@ async function createTextEngine(opts = {}) {
1806
1866
  vertical: asset.align?.vertical ?? "middle"
1807
1867
  },
1808
1868
  background: asset.background,
1809
- glyphPathProvider: (gid) => fonts.glyphPath(desc, gid),
1810
- getUnitsPerEm: () => fonts.getUnitsPerEm(desc)
1869
+ glyphPathProvider: (gid, fontDesc) => fonts.glyphPath(fontDesc || desc, gid),
1870
+ getUnitsPerEm: (fontDesc) => fonts.getUnitsPerEm(fontDesc || desc)
1811
1871
  });
1812
1872
  } catch (err) {
1813
1873
  throw new Error(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shotstack/shotstack-canvas",
3
- "version": "1.3.7",
3
+ "version": "1.3.9",
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",