@shotstack/shotstack-canvas 1.3.8 → 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 = [];
@@ -815,7 +863,7 @@ async function buildDrawOps(p) {
815
863
  const lineIndex = p.lines.indexOf(line);
816
864
  const baselineY = blockY + lineIndex * lineHeightPx;
817
865
  for (const glyph of line.glyphs) {
818
- const path = await p.glyphPathProvider(glyph.id);
866
+ const path = await p.glyphPathProvider(glyph.id, glyph.fontDesc);
819
867
  if (!path || path === "M 0 0") {
820
868
  xCursor += glyph.xAdvance;
821
869
  continue;
@@ -2079,6 +2127,13 @@ async function createTextEngine(opts = {}) {
2079
2127
  const desc = { family: main.family, weight: `${main.weight}`, style: main.style };
2080
2128
  let lines;
2081
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
+ }
2082
2137
  lines = await layout.layout({
2083
2138
  text: asset.text,
2084
2139
  width: asset.width ?? width,
@@ -2086,7 +2141,8 @@ async function createTextEngine(opts = {}) {
2086
2141
  fontSize: main.size,
2087
2142
  lineHeight: asset.style?.lineHeight ?? 1.2,
2088
2143
  desc,
2089
- textTransform: asset.style?.textTransform ?? "none"
2144
+ textTransform: asset.style?.textTransform ?? "none",
2145
+ emojiFallback: emojiAvailable ? emojiDesc : void 0
2090
2146
  });
2091
2147
  } catch (err) {
2092
2148
  throw new Error(
@@ -2128,8 +2184,8 @@ async function createTextEngine(opts = {}) {
2128
2184
  vertical: asset.align?.vertical ?? "middle"
2129
2185
  },
2130
2186
  background: asset.background,
2131
- glyphPathProvider: (gid) => fonts.glyphPath(desc, gid),
2132
- getUnitsPerEm: () => fonts.getUnitsPerEm(desc)
2187
+ glyphPathProvider: (gid, fontDesc) => fonts.glyphPath(fontDesc || desc, gid),
2188
+ getUnitsPerEm: (fontDesc) => fonts.getUnitsPerEm(fontDesc || desc)
2133
2189
  });
2134
2190
  } catch (err) {
2135
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 = [];
@@ -776,7 +824,7 @@ async function buildDrawOps(p) {
776
824
  const lineIndex = p.lines.indexOf(line);
777
825
  const baselineY = blockY + lineIndex * lineHeightPx;
778
826
  for (const glyph of line.glyphs) {
779
- const path = await p.glyphPathProvider(glyph.id);
827
+ const path = await p.glyphPathProvider(glyph.id, glyph.fontDesc);
780
828
  if (!path || path === "M 0 0") {
781
829
  xCursor += glyph.xAdvance;
782
830
  continue;
@@ -2040,6 +2088,13 @@ async function createTextEngine(opts = {}) {
2040
2088
  const desc = { family: main.family, weight: `${main.weight}`, style: main.style };
2041
2089
  let lines;
2042
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
+ }
2043
2098
  lines = await layout.layout({
2044
2099
  text: asset.text,
2045
2100
  width: asset.width ?? width,
@@ -2047,7 +2102,8 @@ async function createTextEngine(opts = {}) {
2047
2102
  fontSize: main.size,
2048
2103
  lineHeight: asset.style?.lineHeight ?? 1.2,
2049
2104
  desc,
2050
- textTransform: asset.style?.textTransform ?? "none"
2105
+ textTransform: asset.style?.textTransform ?? "none",
2106
+ emojiFallback: emojiAvailable ? emojiDesc : void 0
2051
2107
  });
2052
2108
  } catch (err) {
2053
2109
  throw new Error(
@@ -2089,8 +2145,8 @@ async function createTextEngine(opts = {}) {
2089
2145
  vertical: asset.align?.vertical ?? "middle"
2090
2146
  },
2091
2147
  background: asset.background,
2092
- glyphPathProvider: (gid) => fonts.glyphPath(desc, gid),
2093
- getUnitsPerEm: () => fonts.getUnitsPerEm(desc)
2148
+ glyphPathProvider: (gid, fontDesc) => fonts.glyphPath(fontDesc || desc, gid),
2149
+ getUnitsPerEm: (fontDesc) => fonts.getUnitsPerEm(fontDesc || desc)
2094
2150
  });
2095
2151
  } catch (err) {
2096
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 = [];
@@ -781,7 +829,7 @@ async function buildDrawOps(p) {
781
829
  const lineIndex = p.lines.indexOf(line);
782
830
  const baselineY = blockY + lineIndex * lineHeightPx;
783
831
  for (const glyph of line.glyphs) {
784
- const path = await p.glyphPathProvider(glyph.id);
832
+ const path = await p.glyphPathProvider(glyph.id, glyph.fontDesc);
785
833
  if (!path || path === "M 0 0") {
786
834
  xCursor += glyph.xAdvance;
787
835
  continue;
@@ -1761,6 +1809,13 @@ async function createTextEngine(opts = {}) {
1761
1809
  const desc = { family: main.family, weight: `${main.weight}`, style: main.style };
1762
1810
  let lines;
1763
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
+ }
1764
1819
  lines = await layout.layout({
1765
1820
  text: asset.text,
1766
1821
  width: asset.width ?? width,
@@ -1768,7 +1823,8 @@ async function createTextEngine(opts = {}) {
1768
1823
  fontSize: main.size,
1769
1824
  lineHeight: asset.style?.lineHeight ?? 1.2,
1770
1825
  desc,
1771
- textTransform: asset.style?.textTransform ?? "none"
1826
+ textTransform: asset.style?.textTransform ?? "none",
1827
+ emojiFallback: emojiAvailable ? emojiDesc : void 0
1772
1828
  });
1773
1829
  } catch (err) {
1774
1830
  throw new Error(
@@ -1810,8 +1866,8 @@ async function createTextEngine(opts = {}) {
1810
1866
  vertical: asset.align?.vertical ?? "middle"
1811
1867
  },
1812
1868
  background: asset.background,
1813
- glyphPathProvider: (gid) => fonts.glyphPath(desc, gid),
1814
- getUnitsPerEm: () => fonts.getUnitsPerEm(desc)
1869
+ glyphPathProvider: (gid, fontDesc) => fonts.glyphPath(fontDesc || desc, gid),
1870
+ getUnitsPerEm: (fontDesc) => fonts.getUnitsPerEm(fontDesc || desc)
1815
1871
  });
1816
1872
  } catch (err) {
1817
1873
  throw new Error(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shotstack/shotstack-canvas",
3
- "version": "1.3.8",
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",