@shotstack/shotstack-canvas 1.7.1 → 1.8.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/entry.node.cjs +189 -13
- package/dist/entry.node.d.cts +8 -0
- package/dist/entry.node.d.ts +8 -0
- package/dist/entry.node.js +189 -13
- package/dist/entry.web.d.ts +8 -0
- package/dist/entry.web.js +163 -13
- package/package.json +2 -2
package/dist/entry.node.cjs
CHANGED
|
@@ -430,6 +430,9 @@ var FontRegistry = class _FontRegistry {
|
|
|
430
430
|
fonts = /* @__PURE__ */ new Map();
|
|
431
431
|
blobs = /* @__PURE__ */ new Map();
|
|
432
432
|
fontkitFonts = /* @__PURE__ */ new Map();
|
|
433
|
+
fontkitBaseFonts = /* @__PURE__ */ new Map();
|
|
434
|
+
colorEmojiFonts = /* @__PURE__ */ new Set();
|
|
435
|
+
colorEmojiFontBytes = /* @__PURE__ */ new Map();
|
|
433
436
|
wasmBaseURL;
|
|
434
437
|
initPromise;
|
|
435
438
|
emojiFallbackDesc;
|
|
@@ -493,6 +496,49 @@ var FontRegistry = class _FontRegistry {
|
|
|
493
496
|
setEmojiFallback(desc) {
|
|
494
497
|
this.emojiFallbackDesc = desc;
|
|
495
498
|
}
|
|
499
|
+
isColorEmojiFont(family) {
|
|
500
|
+
return this.colorEmojiFonts.has(family);
|
|
501
|
+
}
|
|
502
|
+
getColorEmojiFontBytes(family) {
|
|
503
|
+
return this.colorEmojiFontBytes.get(family);
|
|
504
|
+
}
|
|
505
|
+
getColorEmojiFontFamilies() {
|
|
506
|
+
return Array.from(this.colorEmojiFonts);
|
|
507
|
+
}
|
|
508
|
+
detectColorEmojiFont(fkFont) {
|
|
509
|
+
try {
|
|
510
|
+
if (fkFont.directory && fkFont.directory.tables) {
|
|
511
|
+
const tables = fkFont.directory.tables;
|
|
512
|
+
if (tables.CBDT || tables.CBLC) {
|
|
513
|
+
return true;
|
|
514
|
+
}
|
|
515
|
+
if (tables.sbix) {
|
|
516
|
+
return true;
|
|
517
|
+
}
|
|
518
|
+
if (tables.COLR && tables.CPAL) {
|
|
519
|
+
return true;
|
|
520
|
+
}
|
|
521
|
+
if (tables.SVG) {
|
|
522
|
+
return true;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
if (fkFont.availableTables) {
|
|
526
|
+
const tables = fkFont.availableTables;
|
|
527
|
+
if (tables.includes("CBDT") || tables.includes("CBLC") || tables.includes("sbix") || tables.includes("COLR") && tables.includes("CPAL") || tables.includes("SVG")) {
|
|
528
|
+
return true;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
if (fkFont._tables) {
|
|
532
|
+
const tableNames = Object.keys(fkFont._tables);
|
|
533
|
+
if (tableNames.includes("CBDT") || tableNames.includes("CBLC") || tableNames.includes("sbix") || tableNames.includes("COLR") && tableNames.includes("CPAL") || tableNames.includes("SVG")) {
|
|
534
|
+
return true;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return false;
|
|
538
|
+
} catch {
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
496
542
|
constructor(wasmBaseURL) {
|
|
497
543
|
this.wasmBaseURL = wasmBaseURL;
|
|
498
544
|
}
|
|
@@ -561,13 +607,29 @@ var FontRegistry = class _FontRegistry {
|
|
|
561
607
|
try {
|
|
562
608
|
const buffer = typeof Buffer !== "undefined" ? Buffer.from(bytes) : new Uint8Array(bytes);
|
|
563
609
|
const fkFont = fontkit.create(buffer);
|
|
610
|
+
const baseFontKey = desc.family;
|
|
611
|
+
if (!this.fontkitBaseFonts.has(baseFontKey)) {
|
|
612
|
+
this.fontkitBaseFonts.set(baseFontKey, fkFont);
|
|
613
|
+
}
|
|
614
|
+
const isColorEmojiFont = this.detectColorEmojiFont(fkFont);
|
|
615
|
+
if (isColorEmojiFont) {
|
|
616
|
+
this.colorEmojiFonts.add(desc.family);
|
|
617
|
+
if (!this.colorEmojiFontBytes.has(desc.family)) {
|
|
618
|
+
this.colorEmojiFontBytes.set(desc.family, bytes.slice(0));
|
|
619
|
+
}
|
|
620
|
+
console.log(`\u{1F3A8} Registered color emoji font: ${desc.family}`);
|
|
621
|
+
}
|
|
564
622
|
if (fkFont.variationAxes && Object.keys(fkFont.variationAxes).length > 0) {
|
|
565
623
|
const variationFont = fkFont.getVariation({ wght: weightNum });
|
|
566
624
|
this.fontkitFonts.set(k, variationFont);
|
|
567
|
-
|
|
625
|
+
if (!isColorEmojiFont) {
|
|
626
|
+
console.log(`\u2705 Registered variable font: ${desc.family} weight=${weightNum}`);
|
|
627
|
+
}
|
|
568
628
|
} else {
|
|
569
629
|
this.fontkitFonts.set(k, fkFont);
|
|
570
|
-
|
|
630
|
+
if (!isColorEmojiFont) {
|
|
631
|
+
console.log(`\u2705 Registered static font: ${desc.family}`);
|
|
632
|
+
}
|
|
571
633
|
}
|
|
572
634
|
} catch (err) {
|
|
573
635
|
console.warn(`\u26A0\uFE0F Fontkit failed for ${desc.family}:`, err);
|
|
@@ -606,6 +668,9 @@ var FontRegistry = class _FontRegistry {
|
|
|
606
668
|
const installed = await this.tryFallbackInstall(desc);
|
|
607
669
|
f = installed ? this.fonts.get(k) : void 0;
|
|
608
670
|
}
|
|
671
|
+
if (!f) {
|
|
672
|
+
f = await this.tryDeriveFromExistingFont(desc);
|
|
673
|
+
}
|
|
609
674
|
if (!f) throw new Error(`Font not registered for ${k}`);
|
|
610
675
|
return f;
|
|
611
676
|
} catch (err) {
|
|
@@ -617,6 +682,53 @@ var FontRegistry = class _FontRegistry {
|
|
|
617
682
|
);
|
|
618
683
|
}
|
|
619
684
|
}
|
|
685
|
+
async tryDeriveFromExistingFont(desc) {
|
|
686
|
+
const targetFamily = desc.family;
|
|
687
|
+
const targetWeight = normalizeWeight(desc.weight);
|
|
688
|
+
const targetWeightNum = parseInt(targetWeight, 10);
|
|
689
|
+
let existingBlob;
|
|
690
|
+
for (const [key, blob] of this.blobs) {
|
|
691
|
+
if (key.startsWith(`${targetFamily}__`)) {
|
|
692
|
+
existingBlob = blob;
|
|
693
|
+
break;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
const baseFkFont = this.fontkitBaseFonts.get(targetFamily);
|
|
697
|
+
if (!existingBlob || !baseFkFont) {
|
|
698
|
+
return void 0;
|
|
699
|
+
}
|
|
700
|
+
const hasWeightAxis = baseFkFont.variationAxes && baseFkFont.variationAxes["wght"] !== void 0;
|
|
701
|
+
if (!hasWeightAxis) {
|
|
702
|
+
console.warn(
|
|
703
|
+
`Font ${targetFamily} is not a variable font, cannot derive weight ${targetWeight}`
|
|
704
|
+
);
|
|
705
|
+
return void 0;
|
|
706
|
+
}
|
|
707
|
+
const k = this.key(desc);
|
|
708
|
+
const face = this.hb.createFace(existingBlob, 0);
|
|
709
|
+
const font = this.hb.createFont(face);
|
|
710
|
+
const upem = face.upem || 1e3;
|
|
711
|
+
font.setScale(upem, upem);
|
|
712
|
+
if (typeof font.setVariations === "function") {
|
|
713
|
+
try {
|
|
714
|
+
font.setVariations(`wght=${targetWeightNum}`);
|
|
715
|
+
} catch (e) {
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
this.faces.set(k, face);
|
|
719
|
+
this.fonts.set(k, font);
|
|
720
|
+
try {
|
|
721
|
+
const variationFont = baseFkFont.getVariation({ wght: targetWeightNum });
|
|
722
|
+
this.fontkitFonts.set(k, variationFont);
|
|
723
|
+
console.log(
|
|
724
|
+
`\u2705 Derived variable font: ${targetFamily} weight=${targetWeightNum} from existing registration`
|
|
725
|
+
);
|
|
726
|
+
} catch (err) {
|
|
727
|
+
this.fontkitFonts.set(k, baseFkFont);
|
|
728
|
+
console.warn(`\u26A0\uFE0F Could not apply weight variation, using base font for ${targetFamily}`);
|
|
729
|
+
}
|
|
730
|
+
return font;
|
|
731
|
+
}
|
|
620
732
|
async getFace(desc) {
|
|
621
733
|
try {
|
|
622
734
|
if (!this.hb) await this.init();
|
|
@@ -626,6 +738,10 @@ var FontRegistry = class _FontRegistry {
|
|
|
626
738
|
const installed = await this.tryFallbackInstall(desc);
|
|
627
739
|
face = installed ? this.faces.get(k) : void 0;
|
|
628
740
|
}
|
|
741
|
+
if (!face) {
|
|
742
|
+
await this.tryDeriveFromExistingFont(desc);
|
|
743
|
+
face = this.faces.get(k);
|
|
744
|
+
}
|
|
629
745
|
return face;
|
|
630
746
|
} catch (err) {
|
|
631
747
|
throw new Error(
|
|
@@ -696,6 +812,9 @@ var FontRegistry = class _FontRegistry {
|
|
|
696
812
|
this.faces.clear();
|
|
697
813
|
this.blobs.clear();
|
|
698
814
|
this.fontkitFonts.clear();
|
|
815
|
+
this.fontkitBaseFonts.clear();
|
|
816
|
+
this.colorEmojiFonts.clear();
|
|
817
|
+
this.colorEmojiFontBytes.clear();
|
|
699
818
|
this.hb = void 0;
|
|
700
819
|
this.initPromise = void 0;
|
|
701
820
|
} catch (err) {
|
|
@@ -820,7 +939,10 @@ var LayoutEngine = class {
|
|
|
820
939
|
const charIndex = g.cl;
|
|
821
940
|
let char;
|
|
822
941
|
if (charIndex >= 0 && charIndex < input.length) {
|
|
823
|
-
|
|
942
|
+
const codePoint = input.codePointAt(charIndex);
|
|
943
|
+
if (codePoint !== void 0) {
|
|
944
|
+
char = String.fromCodePoint(codePoint);
|
|
945
|
+
}
|
|
824
946
|
}
|
|
825
947
|
return {
|
|
826
948
|
id: g.g,
|
|
@@ -1005,13 +1127,27 @@ async function buildDrawOps(p) {
|
|
|
1005
1127
|
});
|
|
1006
1128
|
}
|
|
1007
1129
|
for (const glyph of line.glyphs) {
|
|
1130
|
+
const glyphX = xCursor + glyph.xOffset;
|
|
1131
|
+
const glyphY = baselineY + glyph.yOffset;
|
|
1132
|
+
const glyphFamily = glyph.fontDesc?.family;
|
|
1133
|
+
const isColorEmoji = glyph.isColorEmoji || glyphFamily && p.isColorEmojiFont && p.isColorEmojiFont(glyphFamily);
|
|
1134
|
+
if (isColorEmoji && glyph.char) {
|
|
1135
|
+
textOps.push({
|
|
1136
|
+
op: "DrawColorEmoji",
|
|
1137
|
+
char: glyph.char,
|
|
1138
|
+
x: glyphX,
|
|
1139
|
+
y: glyphY,
|
|
1140
|
+
fontSize: p.font.size,
|
|
1141
|
+
fontFamily: glyphFamily || "NotoColorEmoji"
|
|
1142
|
+
});
|
|
1143
|
+
xCursor += glyph.xAdvance;
|
|
1144
|
+
continue;
|
|
1145
|
+
}
|
|
1008
1146
|
const path = await p.glyphPathProvider(glyph.id, glyph.fontDesc);
|
|
1009
1147
|
if (!path || path === "M 0 0") {
|
|
1010
1148
|
xCursor += glyph.xAdvance;
|
|
1011
1149
|
continue;
|
|
1012
1150
|
}
|
|
1013
|
-
const glyphX = xCursor + glyph.xOffset;
|
|
1014
|
-
const glyphY = baselineY + glyph.yOffset;
|
|
1015
1151
|
const pb = computePathBounds(path);
|
|
1016
1152
|
const x1 = glyphX + scale * pb.x;
|
|
1017
1153
|
const x2 = glyphX + scale * (pb.x + pb.w);
|
|
@@ -1986,6 +2122,16 @@ async function createNodePainter(opts) {
|
|
|
1986
2122
|
});
|
|
1987
2123
|
continue;
|
|
1988
2124
|
}
|
|
2125
|
+
if (op.op === "DrawColorEmoji") {
|
|
2126
|
+
renderToBoth((context) => {
|
|
2127
|
+
context.save();
|
|
2128
|
+
context.font = `${op.fontSize}px "${op.fontFamily}"`;
|
|
2129
|
+
context.textBaseline = "alphabetic";
|
|
2130
|
+
context.fillText(op.char, op.x, op.y);
|
|
2131
|
+
context.restore();
|
|
2132
|
+
});
|
|
2133
|
+
continue;
|
|
2134
|
+
}
|
|
1989
2135
|
}
|
|
1990
2136
|
if (needsAlphaExtraction) {
|
|
1991
2137
|
const whiteData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
@@ -2452,6 +2598,24 @@ var isGlyphFill2 = (op) => {
|
|
|
2452
2598
|
};
|
|
2453
2599
|
|
|
2454
2600
|
// src/env/entry.node.ts
|
|
2601
|
+
var registeredGlobalFonts = /* @__PURE__ */ new Set();
|
|
2602
|
+
async function registerColorEmojiWithCanvas(family, bytes) {
|
|
2603
|
+
if (registeredGlobalFonts.has(family)) {
|
|
2604
|
+
return;
|
|
2605
|
+
}
|
|
2606
|
+
try {
|
|
2607
|
+
const canvasMod = await import("canvas");
|
|
2608
|
+
const GlobalFonts = canvasMod.GlobalFonts;
|
|
2609
|
+
if (GlobalFonts && typeof GlobalFonts.register === "function") {
|
|
2610
|
+
const buffer = Buffer.from(bytes);
|
|
2611
|
+
GlobalFonts.register(buffer, family);
|
|
2612
|
+
registeredGlobalFonts.add(family);
|
|
2613
|
+
console.log(`\u{1F3A8} Registered color emoji font with canvas: ${family}`);
|
|
2614
|
+
}
|
|
2615
|
+
} catch (err) {
|
|
2616
|
+
console.warn(`\u26A0\uFE0F Could not register color emoji font with canvas GlobalFonts: ${err}`);
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2455
2619
|
async function createTextEngine(opts = {}) {
|
|
2456
2620
|
const width = opts.width ?? CANVAS_CONFIG.DEFAULTS.width;
|
|
2457
2621
|
const height = opts.height ?? CANVAS_CONFIG.DEFAULTS.height;
|
|
@@ -2478,6 +2642,12 @@ async function createTextEngine(opts = {}) {
|
|
|
2478
2642
|
family: cf.family,
|
|
2479
2643
|
weight: cf.weight ?? "400"
|
|
2480
2644
|
});
|
|
2645
|
+
if (fonts.isColorEmojiFont(cf.family)) {
|
|
2646
|
+
const emojiBytes = fonts.getColorEmojiFontBytes(cf.family);
|
|
2647
|
+
if (emojiBytes) {
|
|
2648
|
+
await registerColorEmojiWithCanvas(cf.family, emojiBytes);
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2481
2651
|
} catch (err) {
|
|
2482
2652
|
throw new Error(
|
|
2483
2653
|
`Failed to load custom font "${cf.family}" from ${cf.src}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -2535,12 +2705,17 @@ async function createTextEngine(opts = {}) {
|
|
|
2535
2705
|
const desc = { family: main.family, weight: `${main.weight}` };
|
|
2536
2706
|
let lines;
|
|
2537
2707
|
try {
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2708
|
+
let emojiFallback = void 0;
|
|
2709
|
+
const colorEmojiFamilies = fonts.getColorEmojiFontFamilies();
|
|
2710
|
+
if (colorEmojiFamilies.length > 0) {
|
|
2711
|
+
emojiFallback = { family: colorEmojiFamilies[0], weight: "400" };
|
|
2712
|
+
} else {
|
|
2713
|
+
const notoEmojiDesc = { family: "NotoEmoji", weight: "400" };
|
|
2714
|
+
try {
|
|
2715
|
+
await fonts.getFace(notoEmojiDesc);
|
|
2716
|
+
emojiFallback = notoEmojiDesc;
|
|
2717
|
+
} catch {
|
|
2718
|
+
}
|
|
2544
2719
|
}
|
|
2545
2720
|
const padding2 = asset.padding ? typeof asset.padding === "number" ? {
|
|
2546
2721
|
top: asset.padding,
|
|
@@ -2556,7 +2731,7 @@ async function createTextEngine(opts = {}) {
|
|
|
2556
2731
|
lineHeight: asset.style?.lineHeight ?? 1.2,
|
|
2557
2732
|
desc,
|
|
2558
2733
|
textTransform: asset.style?.textTransform ?? "none",
|
|
2559
|
-
emojiFallback
|
|
2734
|
+
emojiFallback
|
|
2560
2735
|
});
|
|
2561
2736
|
} catch (err) {
|
|
2562
2737
|
throw new Error(
|
|
@@ -2611,7 +2786,8 @@ async function createTextEngine(opts = {}) {
|
|
|
2611
2786
|
border: asset.border,
|
|
2612
2787
|
padding: asset.padding,
|
|
2613
2788
|
glyphPathProvider: (gid, fontDesc) => fonts.glyphPath(fontDesc || desc, gid),
|
|
2614
|
-
getUnitsPerEm: (fontDesc) => fonts.getUnitsPerEm(fontDesc || desc)
|
|
2789
|
+
getUnitsPerEm: (fontDesc) => fonts.getUnitsPerEm(fontDesc || desc),
|
|
2790
|
+
isColorEmojiFont: (family) => fonts.isColorEmojiFont(family)
|
|
2615
2791
|
});
|
|
2616
2792
|
} catch (err) {
|
|
2617
2793
|
throw new Error(
|
package/dist/entry.node.d.cts
CHANGED
|
@@ -156,6 +156,7 @@ type Glyph = {
|
|
|
156
156
|
family: string;
|
|
157
157
|
weight?: string | number;
|
|
158
158
|
};
|
|
159
|
+
isColorEmoji?: boolean;
|
|
159
160
|
};
|
|
160
161
|
type ShapedLine = {
|
|
161
162
|
glyphs: Glyph[];
|
|
@@ -229,6 +230,13 @@ type DrawOp = {
|
|
|
229
230
|
opacity: number;
|
|
230
231
|
};
|
|
231
232
|
borderRadius?: number;
|
|
233
|
+
} | {
|
|
234
|
+
op: "DrawColorEmoji";
|
|
235
|
+
char: string;
|
|
236
|
+
x: number;
|
|
237
|
+
y: number;
|
|
238
|
+
fontSize: number;
|
|
239
|
+
fontFamily: string;
|
|
232
240
|
};
|
|
233
241
|
type EngineInit = {
|
|
234
242
|
width: number;
|
package/dist/entry.node.d.ts
CHANGED
|
@@ -156,6 +156,7 @@ type Glyph = {
|
|
|
156
156
|
family: string;
|
|
157
157
|
weight?: string | number;
|
|
158
158
|
};
|
|
159
|
+
isColorEmoji?: boolean;
|
|
159
160
|
};
|
|
160
161
|
type ShapedLine = {
|
|
161
162
|
glyphs: Glyph[];
|
|
@@ -229,6 +230,13 @@ type DrawOp = {
|
|
|
229
230
|
opacity: number;
|
|
230
231
|
};
|
|
231
232
|
borderRadius?: number;
|
|
233
|
+
} | {
|
|
234
|
+
op: "DrawColorEmoji";
|
|
235
|
+
char: string;
|
|
236
|
+
x: number;
|
|
237
|
+
y: number;
|
|
238
|
+
fontSize: number;
|
|
239
|
+
fontFamily: string;
|
|
232
240
|
};
|
|
233
241
|
type EngineInit = {
|
|
234
242
|
width: number;
|
package/dist/entry.node.js
CHANGED
|
@@ -399,6 +399,9 @@ var FontRegistry = class _FontRegistry {
|
|
|
399
399
|
fonts = /* @__PURE__ */ new Map();
|
|
400
400
|
blobs = /* @__PURE__ */ new Map();
|
|
401
401
|
fontkitFonts = /* @__PURE__ */ new Map();
|
|
402
|
+
fontkitBaseFonts = /* @__PURE__ */ new Map();
|
|
403
|
+
colorEmojiFonts = /* @__PURE__ */ new Set();
|
|
404
|
+
colorEmojiFontBytes = /* @__PURE__ */ new Map();
|
|
402
405
|
wasmBaseURL;
|
|
403
406
|
initPromise;
|
|
404
407
|
emojiFallbackDesc;
|
|
@@ -462,6 +465,49 @@ var FontRegistry = class _FontRegistry {
|
|
|
462
465
|
setEmojiFallback(desc) {
|
|
463
466
|
this.emojiFallbackDesc = desc;
|
|
464
467
|
}
|
|
468
|
+
isColorEmojiFont(family) {
|
|
469
|
+
return this.colorEmojiFonts.has(family);
|
|
470
|
+
}
|
|
471
|
+
getColorEmojiFontBytes(family) {
|
|
472
|
+
return this.colorEmojiFontBytes.get(family);
|
|
473
|
+
}
|
|
474
|
+
getColorEmojiFontFamilies() {
|
|
475
|
+
return Array.from(this.colorEmojiFonts);
|
|
476
|
+
}
|
|
477
|
+
detectColorEmojiFont(fkFont) {
|
|
478
|
+
try {
|
|
479
|
+
if (fkFont.directory && fkFont.directory.tables) {
|
|
480
|
+
const tables = fkFont.directory.tables;
|
|
481
|
+
if (tables.CBDT || tables.CBLC) {
|
|
482
|
+
return true;
|
|
483
|
+
}
|
|
484
|
+
if (tables.sbix) {
|
|
485
|
+
return true;
|
|
486
|
+
}
|
|
487
|
+
if (tables.COLR && tables.CPAL) {
|
|
488
|
+
return true;
|
|
489
|
+
}
|
|
490
|
+
if (tables.SVG) {
|
|
491
|
+
return true;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
if (fkFont.availableTables) {
|
|
495
|
+
const tables = fkFont.availableTables;
|
|
496
|
+
if (tables.includes("CBDT") || tables.includes("CBLC") || tables.includes("sbix") || tables.includes("COLR") && tables.includes("CPAL") || tables.includes("SVG")) {
|
|
497
|
+
return true;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if (fkFont._tables) {
|
|
501
|
+
const tableNames = Object.keys(fkFont._tables);
|
|
502
|
+
if (tableNames.includes("CBDT") || tableNames.includes("CBLC") || tableNames.includes("sbix") || tableNames.includes("COLR") && tableNames.includes("CPAL") || tableNames.includes("SVG")) {
|
|
503
|
+
return true;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return false;
|
|
507
|
+
} catch {
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
465
511
|
constructor(wasmBaseURL) {
|
|
466
512
|
this.wasmBaseURL = wasmBaseURL;
|
|
467
513
|
}
|
|
@@ -530,13 +576,29 @@ var FontRegistry = class _FontRegistry {
|
|
|
530
576
|
try {
|
|
531
577
|
const buffer = typeof Buffer !== "undefined" ? Buffer.from(bytes) : new Uint8Array(bytes);
|
|
532
578
|
const fkFont = fontkit.create(buffer);
|
|
579
|
+
const baseFontKey = desc.family;
|
|
580
|
+
if (!this.fontkitBaseFonts.has(baseFontKey)) {
|
|
581
|
+
this.fontkitBaseFonts.set(baseFontKey, fkFont);
|
|
582
|
+
}
|
|
583
|
+
const isColorEmojiFont = this.detectColorEmojiFont(fkFont);
|
|
584
|
+
if (isColorEmojiFont) {
|
|
585
|
+
this.colorEmojiFonts.add(desc.family);
|
|
586
|
+
if (!this.colorEmojiFontBytes.has(desc.family)) {
|
|
587
|
+
this.colorEmojiFontBytes.set(desc.family, bytes.slice(0));
|
|
588
|
+
}
|
|
589
|
+
console.log(`\u{1F3A8} Registered color emoji font: ${desc.family}`);
|
|
590
|
+
}
|
|
533
591
|
if (fkFont.variationAxes && Object.keys(fkFont.variationAxes).length > 0) {
|
|
534
592
|
const variationFont = fkFont.getVariation({ wght: weightNum });
|
|
535
593
|
this.fontkitFonts.set(k, variationFont);
|
|
536
|
-
|
|
594
|
+
if (!isColorEmojiFont) {
|
|
595
|
+
console.log(`\u2705 Registered variable font: ${desc.family} weight=${weightNum}`);
|
|
596
|
+
}
|
|
537
597
|
} else {
|
|
538
598
|
this.fontkitFonts.set(k, fkFont);
|
|
539
|
-
|
|
599
|
+
if (!isColorEmojiFont) {
|
|
600
|
+
console.log(`\u2705 Registered static font: ${desc.family}`);
|
|
601
|
+
}
|
|
540
602
|
}
|
|
541
603
|
} catch (err) {
|
|
542
604
|
console.warn(`\u26A0\uFE0F Fontkit failed for ${desc.family}:`, err);
|
|
@@ -575,6 +637,9 @@ var FontRegistry = class _FontRegistry {
|
|
|
575
637
|
const installed = await this.tryFallbackInstall(desc);
|
|
576
638
|
f = installed ? this.fonts.get(k) : void 0;
|
|
577
639
|
}
|
|
640
|
+
if (!f) {
|
|
641
|
+
f = await this.tryDeriveFromExistingFont(desc);
|
|
642
|
+
}
|
|
578
643
|
if (!f) throw new Error(`Font not registered for ${k}`);
|
|
579
644
|
return f;
|
|
580
645
|
} catch (err) {
|
|
@@ -586,6 +651,53 @@ var FontRegistry = class _FontRegistry {
|
|
|
586
651
|
);
|
|
587
652
|
}
|
|
588
653
|
}
|
|
654
|
+
async tryDeriveFromExistingFont(desc) {
|
|
655
|
+
const targetFamily = desc.family;
|
|
656
|
+
const targetWeight = normalizeWeight(desc.weight);
|
|
657
|
+
const targetWeightNum = parseInt(targetWeight, 10);
|
|
658
|
+
let existingBlob;
|
|
659
|
+
for (const [key, blob] of this.blobs) {
|
|
660
|
+
if (key.startsWith(`${targetFamily}__`)) {
|
|
661
|
+
existingBlob = blob;
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
const baseFkFont = this.fontkitBaseFonts.get(targetFamily);
|
|
666
|
+
if (!existingBlob || !baseFkFont) {
|
|
667
|
+
return void 0;
|
|
668
|
+
}
|
|
669
|
+
const hasWeightAxis = baseFkFont.variationAxes && baseFkFont.variationAxes["wght"] !== void 0;
|
|
670
|
+
if (!hasWeightAxis) {
|
|
671
|
+
console.warn(
|
|
672
|
+
`Font ${targetFamily} is not a variable font, cannot derive weight ${targetWeight}`
|
|
673
|
+
);
|
|
674
|
+
return void 0;
|
|
675
|
+
}
|
|
676
|
+
const k = this.key(desc);
|
|
677
|
+
const face = this.hb.createFace(existingBlob, 0);
|
|
678
|
+
const font = this.hb.createFont(face);
|
|
679
|
+
const upem = face.upem || 1e3;
|
|
680
|
+
font.setScale(upem, upem);
|
|
681
|
+
if (typeof font.setVariations === "function") {
|
|
682
|
+
try {
|
|
683
|
+
font.setVariations(`wght=${targetWeightNum}`);
|
|
684
|
+
} catch (e) {
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
this.faces.set(k, face);
|
|
688
|
+
this.fonts.set(k, font);
|
|
689
|
+
try {
|
|
690
|
+
const variationFont = baseFkFont.getVariation({ wght: targetWeightNum });
|
|
691
|
+
this.fontkitFonts.set(k, variationFont);
|
|
692
|
+
console.log(
|
|
693
|
+
`\u2705 Derived variable font: ${targetFamily} weight=${targetWeightNum} from existing registration`
|
|
694
|
+
);
|
|
695
|
+
} catch (err) {
|
|
696
|
+
this.fontkitFonts.set(k, baseFkFont);
|
|
697
|
+
console.warn(`\u26A0\uFE0F Could not apply weight variation, using base font for ${targetFamily}`);
|
|
698
|
+
}
|
|
699
|
+
return font;
|
|
700
|
+
}
|
|
589
701
|
async getFace(desc) {
|
|
590
702
|
try {
|
|
591
703
|
if (!this.hb) await this.init();
|
|
@@ -595,6 +707,10 @@ var FontRegistry = class _FontRegistry {
|
|
|
595
707
|
const installed = await this.tryFallbackInstall(desc);
|
|
596
708
|
face = installed ? this.faces.get(k) : void 0;
|
|
597
709
|
}
|
|
710
|
+
if (!face) {
|
|
711
|
+
await this.tryDeriveFromExistingFont(desc);
|
|
712
|
+
face = this.faces.get(k);
|
|
713
|
+
}
|
|
598
714
|
return face;
|
|
599
715
|
} catch (err) {
|
|
600
716
|
throw new Error(
|
|
@@ -665,6 +781,9 @@ var FontRegistry = class _FontRegistry {
|
|
|
665
781
|
this.faces.clear();
|
|
666
782
|
this.blobs.clear();
|
|
667
783
|
this.fontkitFonts.clear();
|
|
784
|
+
this.fontkitBaseFonts.clear();
|
|
785
|
+
this.colorEmojiFonts.clear();
|
|
786
|
+
this.colorEmojiFontBytes.clear();
|
|
668
787
|
this.hb = void 0;
|
|
669
788
|
this.initPromise = void 0;
|
|
670
789
|
} catch (err) {
|
|
@@ -789,7 +908,10 @@ var LayoutEngine = class {
|
|
|
789
908
|
const charIndex = g.cl;
|
|
790
909
|
let char;
|
|
791
910
|
if (charIndex >= 0 && charIndex < input.length) {
|
|
792
|
-
|
|
911
|
+
const codePoint = input.codePointAt(charIndex);
|
|
912
|
+
if (codePoint !== void 0) {
|
|
913
|
+
char = String.fromCodePoint(codePoint);
|
|
914
|
+
}
|
|
793
915
|
}
|
|
794
916
|
return {
|
|
795
917
|
id: g.g,
|
|
@@ -974,13 +1096,27 @@ async function buildDrawOps(p) {
|
|
|
974
1096
|
});
|
|
975
1097
|
}
|
|
976
1098
|
for (const glyph of line.glyphs) {
|
|
1099
|
+
const glyphX = xCursor + glyph.xOffset;
|
|
1100
|
+
const glyphY = baselineY + glyph.yOffset;
|
|
1101
|
+
const glyphFamily = glyph.fontDesc?.family;
|
|
1102
|
+
const isColorEmoji = glyph.isColorEmoji || glyphFamily && p.isColorEmojiFont && p.isColorEmojiFont(glyphFamily);
|
|
1103
|
+
if (isColorEmoji && glyph.char) {
|
|
1104
|
+
textOps.push({
|
|
1105
|
+
op: "DrawColorEmoji",
|
|
1106
|
+
char: glyph.char,
|
|
1107
|
+
x: glyphX,
|
|
1108
|
+
y: glyphY,
|
|
1109
|
+
fontSize: p.font.size,
|
|
1110
|
+
fontFamily: glyphFamily || "NotoColorEmoji"
|
|
1111
|
+
});
|
|
1112
|
+
xCursor += glyph.xAdvance;
|
|
1113
|
+
continue;
|
|
1114
|
+
}
|
|
977
1115
|
const path = await p.glyphPathProvider(glyph.id, glyph.fontDesc);
|
|
978
1116
|
if (!path || path === "M 0 0") {
|
|
979
1117
|
xCursor += glyph.xAdvance;
|
|
980
1118
|
continue;
|
|
981
1119
|
}
|
|
982
|
-
const glyphX = xCursor + glyph.xOffset;
|
|
983
|
-
const glyphY = baselineY + glyph.yOffset;
|
|
984
1120
|
const pb = computePathBounds(path);
|
|
985
1121
|
const x1 = glyphX + scale * pb.x;
|
|
986
1122
|
const x2 = glyphX + scale * (pb.x + pb.w);
|
|
@@ -1955,6 +2091,16 @@ async function createNodePainter(opts) {
|
|
|
1955
2091
|
});
|
|
1956
2092
|
continue;
|
|
1957
2093
|
}
|
|
2094
|
+
if (op.op === "DrawColorEmoji") {
|
|
2095
|
+
renderToBoth((context) => {
|
|
2096
|
+
context.save();
|
|
2097
|
+
context.font = `${op.fontSize}px "${op.fontFamily}"`;
|
|
2098
|
+
context.textBaseline = "alphabetic";
|
|
2099
|
+
context.fillText(op.char, op.x, op.y);
|
|
2100
|
+
context.restore();
|
|
2101
|
+
});
|
|
2102
|
+
continue;
|
|
2103
|
+
}
|
|
1958
2104
|
}
|
|
1959
2105
|
if (needsAlphaExtraction) {
|
|
1960
2106
|
const whiteData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
@@ -2421,6 +2567,24 @@ var isGlyphFill2 = (op) => {
|
|
|
2421
2567
|
};
|
|
2422
2568
|
|
|
2423
2569
|
// src/env/entry.node.ts
|
|
2570
|
+
var registeredGlobalFonts = /* @__PURE__ */ new Set();
|
|
2571
|
+
async function registerColorEmojiWithCanvas(family, bytes) {
|
|
2572
|
+
if (registeredGlobalFonts.has(family)) {
|
|
2573
|
+
return;
|
|
2574
|
+
}
|
|
2575
|
+
try {
|
|
2576
|
+
const canvasMod = await import("canvas");
|
|
2577
|
+
const GlobalFonts = canvasMod.GlobalFonts;
|
|
2578
|
+
if (GlobalFonts && typeof GlobalFonts.register === "function") {
|
|
2579
|
+
const buffer = Buffer.from(bytes);
|
|
2580
|
+
GlobalFonts.register(buffer, family);
|
|
2581
|
+
registeredGlobalFonts.add(family);
|
|
2582
|
+
console.log(`\u{1F3A8} Registered color emoji font with canvas: ${family}`);
|
|
2583
|
+
}
|
|
2584
|
+
} catch (err) {
|
|
2585
|
+
console.warn(`\u26A0\uFE0F Could not register color emoji font with canvas GlobalFonts: ${err}`);
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2424
2588
|
async function createTextEngine(opts = {}) {
|
|
2425
2589
|
const width = opts.width ?? CANVAS_CONFIG.DEFAULTS.width;
|
|
2426
2590
|
const height = opts.height ?? CANVAS_CONFIG.DEFAULTS.height;
|
|
@@ -2447,6 +2611,12 @@ async function createTextEngine(opts = {}) {
|
|
|
2447
2611
|
family: cf.family,
|
|
2448
2612
|
weight: cf.weight ?? "400"
|
|
2449
2613
|
});
|
|
2614
|
+
if (fonts.isColorEmojiFont(cf.family)) {
|
|
2615
|
+
const emojiBytes = fonts.getColorEmojiFontBytes(cf.family);
|
|
2616
|
+
if (emojiBytes) {
|
|
2617
|
+
await registerColorEmojiWithCanvas(cf.family, emojiBytes);
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2450
2620
|
} catch (err) {
|
|
2451
2621
|
throw new Error(
|
|
2452
2622
|
`Failed to load custom font "${cf.family}" from ${cf.src}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -2504,12 +2674,17 @@ async function createTextEngine(opts = {}) {
|
|
|
2504
2674
|
const desc = { family: main.family, weight: `${main.weight}` };
|
|
2505
2675
|
let lines;
|
|
2506
2676
|
try {
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2677
|
+
let emojiFallback = void 0;
|
|
2678
|
+
const colorEmojiFamilies = fonts.getColorEmojiFontFamilies();
|
|
2679
|
+
if (colorEmojiFamilies.length > 0) {
|
|
2680
|
+
emojiFallback = { family: colorEmojiFamilies[0], weight: "400" };
|
|
2681
|
+
} else {
|
|
2682
|
+
const notoEmojiDesc = { family: "NotoEmoji", weight: "400" };
|
|
2683
|
+
try {
|
|
2684
|
+
await fonts.getFace(notoEmojiDesc);
|
|
2685
|
+
emojiFallback = notoEmojiDesc;
|
|
2686
|
+
} catch {
|
|
2687
|
+
}
|
|
2513
2688
|
}
|
|
2514
2689
|
const padding2 = asset.padding ? typeof asset.padding === "number" ? {
|
|
2515
2690
|
top: asset.padding,
|
|
@@ -2525,7 +2700,7 @@ async function createTextEngine(opts = {}) {
|
|
|
2525
2700
|
lineHeight: asset.style?.lineHeight ?? 1.2,
|
|
2526
2701
|
desc,
|
|
2527
2702
|
textTransform: asset.style?.textTransform ?? "none",
|
|
2528
|
-
emojiFallback
|
|
2703
|
+
emojiFallback
|
|
2529
2704
|
});
|
|
2530
2705
|
} catch (err) {
|
|
2531
2706
|
throw new Error(
|
|
@@ -2580,7 +2755,8 @@ async function createTextEngine(opts = {}) {
|
|
|
2580
2755
|
border: asset.border,
|
|
2581
2756
|
padding: asset.padding,
|
|
2582
2757
|
glyphPathProvider: (gid, fontDesc) => fonts.glyphPath(fontDesc || desc, gid),
|
|
2583
|
-
getUnitsPerEm: (fontDesc) => fonts.getUnitsPerEm(fontDesc || desc)
|
|
2758
|
+
getUnitsPerEm: (fontDesc) => fonts.getUnitsPerEm(fontDesc || desc),
|
|
2759
|
+
isColorEmojiFont: (family) => fonts.isColorEmojiFont(family)
|
|
2584
2760
|
});
|
|
2585
2761
|
} catch (err) {
|
|
2586
2762
|
throw new Error(
|
package/dist/entry.web.d.ts
CHANGED
|
@@ -156,6 +156,7 @@ type Glyph = {
|
|
|
156
156
|
family: string;
|
|
157
157
|
weight?: string | number;
|
|
158
158
|
};
|
|
159
|
+
isColorEmoji?: boolean;
|
|
159
160
|
};
|
|
160
161
|
type ShapedLine = {
|
|
161
162
|
glyphs: Glyph[];
|
|
@@ -229,6 +230,13 @@ type DrawOp = {
|
|
|
229
230
|
opacity: number;
|
|
230
231
|
};
|
|
231
232
|
borderRadius?: number;
|
|
233
|
+
} | {
|
|
234
|
+
op: "DrawColorEmoji";
|
|
235
|
+
char: string;
|
|
236
|
+
x: number;
|
|
237
|
+
y: number;
|
|
238
|
+
fontSize: number;
|
|
239
|
+
fontFamily: string;
|
|
232
240
|
};
|
|
233
241
|
type EngineInit = {
|
|
234
242
|
width: number;
|
package/dist/entry.web.js
CHANGED
|
@@ -404,6 +404,9 @@ var _FontRegistry = class _FontRegistry {
|
|
|
404
404
|
__publicField(this, "fonts", /* @__PURE__ */ new Map());
|
|
405
405
|
__publicField(this, "blobs", /* @__PURE__ */ new Map());
|
|
406
406
|
__publicField(this, "fontkitFonts", /* @__PURE__ */ new Map());
|
|
407
|
+
__publicField(this, "fontkitBaseFonts", /* @__PURE__ */ new Map());
|
|
408
|
+
__publicField(this, "colorEmojiFonts", /* @__PURE__ */ new Set());
|
|
409
|
+
__publicField(this, "colorEmojiFontBytes", /* @__PURE__ */ new Map());
|
|
407
410
|
__publicField(this, "wasmBaseURL");
|
|
408
411
|
__publicField(this, "initPromise");
|
|
409
412
|
__publicField(this, "emojiFallbackDesc");
|
|
@@ -465,6 +468,49 @@ var _FontRegistry = class _FontRegistry {
|
|
|
465
468
|
setEmojiFallback(desc) {
|
|
466
469
|
this.emojiFallbackDesc = desc;
|
|
467
470
|
}
|
|
471
|
+
isColorEmojiFont(family) {
|
|
472
|
+
return this.colorEmojiFonts.has(family);
|
|
473
|
+
}
|
|
474
|
+
getColorEmojiFontBytes(family) {
|
|
475
|
+
return this.colorEmojiFontBytes.get(family);
|
|
476
|
+
}
|
|
477
|
+
getColorEmojiFontFamilies() {
|
|
478
|
+
return Array.from(this.colorEmojiFonts);
|
|
479
|
+
}
|
|
480
|
+
detectColorEmojiFont(fkFont) {
|
|
481
|
+
try {
|
|
482
|
+
if (fkFont.directory && fkFont.directory.tables) {
|
|
483
|
+
const tables = fkFont.directory.tables;
|
|
484
|
+
if (tables.CBDT || tables.CBLC) {
|
|
485
|
+
return true;
|
|
486
|
+
}
|
|
487
|
+
if (tables.sbix) {
|
|
488
|
+
return true;
|
|
489
|
+
}
|
|
490
|
+
if (tables.COLR && tables.CPAL) {
|
|
491
|
+
return true;
|
|
492
|
+
}
|
|
493
|
+
if (tables.SVG) {
|
|
494
|
+
return true;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (fkFont.availableTables) {
|
|
498
|
+
const tables = fkFont.availableTables;
|
|
499
|
+
if (tables.includes("CBDT") || tables.includes("CBLC") || tables.includes("sbix") || tables.includes("COLR") && tables.includes("CPAL") || tables.includes("SVG")) {
|
|
500
|
+
return true;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
if (fkFont._tables) {
|
|
504
|
+
const tableNames = Object.keys(fkFont._tables);
|
|
505
|
+
if (tableNames.includes("CBDT") || tableNames.includes("CBLC") || tableNames.includes("sbix") || tableNames.includes("COLR") && tableNames.includes("CPAL") || tableNames.includes("SVG")) {
|
|
506
|
+
return true;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return false;
|
|
510
|
+
} catch {
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
468
514
|
async init() {
|
|
469
515
|
if (this.initPromise) {
|
|
470
516
|
await this.initPromise;
|
|
@@ -530,13 +576,29 @@ var _FontRegistry = class _FontRegistry {
|
|
|
530
576
|
try {
|
|
531
577
|
const buffer = typeof Buffer !== "undefined" ? Buffer.from(bytes) : new Uint8Array(bytes);
|
|
532
578
|
const fkFont = fontkit.create(buffer);
|
|
579
|
+
const baseFontKey = desc.family;
|
|
580
|
+
if (!this.fontkitBaseFonts.has(baseFontKey)) {
|
|
581
|
+
this.fontkitBaseFonts.set(baseFontKey, fkFont);
|
|
582
|
+
}
|
|
583
|
+
const isColorEmojiFont = this.detectColorEmojiFont(fkFont);
|
|
584
|
+
if (isColorEmojiFont) {
|
|
585
|
+
this.colorEmojiFonts.add(desc.family);
|
|
586
|
+
if (!this.colorEmojiFontBytes.has(desc.family)) {
|
|
587
|
+
this.colorEmojiFontBytes.set(desc.family, bytes.slice(0));
|
|
588
|
+
}
|
|
589
|
+
console.log(`\u{1F3A8} Registered color emoji font: ${desc.family}`);
|
|
590
|
+
}
|
|
533
591
|
if (fkFont.variationAxes && Object.keys(fkFont.variationAxes).length > 0) {
|
|
534
592
|
const variationFont = fkFont.getVariation({ wght: weightNum });
|
|
535
593
|
this.fontkitFonts.set(k, variationFont);
|
|
536
|
-
|
|
594
|
+
if (!isColorEmojiFont) {
|
|
595
|
+
console.log(`\u2705 Registered variable font: ${desc.family} weight=${weightNum}`);
|
|
596
|
+
}
|
|
537
597
|
} else {
|
|
538
598
|
this.fontkitFonts.set(k, fkFont);
|
|
539
|
-
|
|
599
|
+
if (!isColorEmojiFont) {
|
|
600
|
+
console.log(`\u2705 Registered static font: ${desc.family}`);
|
|
601
|
+
}
|
|
540
602
|
}
|
|
541
603
|
} catch (err) {
|
|
542
604
|
console.warn(`\u26A0\uFE0F Fontkit failed for ${desc.family}:`, err);
|
|
@@ -575,6 +637,9 @@ var _FontRegistry = class _FontRegistry {
|
|
|
575
637
|
const installed = await this.tryFallbackInstall(desc);
|
|
576
638
|
f = installed ? this.fonts.get(k) : void 0;
|
|
577
639
|
}
|
|
640
|
+
if (!f) {
|
|
641
|
+
f = await this.tryDeriveFromExistingFont(desc);
|
|
642
|
+
}
|
|
578
643
|
if (!f) throw new Error(`Font not registered for ${k}`);
|
|
579
644
|
return f;
|
|
580
645
|
} catch (err) {
|
|
@@ -586,6 +651,53 @@ var _FontRegistry = class _FontRegistry {
|
|
|
586
651
|
);
|
|
587
652
|
}
|
|
588
653
|
}
|
|
654
|
+
async tryDeriveFromExistingFont(desc) {
|
|
655
|
+
const targetFamily = desc.family;
|
|
656
|
+
const targetWeight = normalizeWeight(desc.weight);
|
|
657
|
+
const targetWeightNum = parseInt(targetWeight, 10);
|
|
658
|
+
let existingBlob;
|
|
659
|
+
for (const [key, blob] of this.blobs) {
|
|
660
|
+
if (key.startsWith(`${targetFamily}__`)) {
|
|
661
|
+
existingBlob = blob;
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
const baseFkFont = this.fontkitBaseFonts.get(targetFamily);
|
|
666
|
+
if (!existingBlob || !baseFkFont) {
|
|
667
|
+
return void 0;
|
|
668
|
+
}
|
|
669
|
+
const hasWeightAxis = baseFkFont.variationAxes && baseFkFont.variationAxes["wght"] !== void 0;
|
|
670
|
+
if (!hasWeightAxis) {
|
|
671
|
+
console.warn(
|
|
672
|
+
`Font ${targetFamily} is not a variable font, cannot derive weight ${targetWeight}`
|
|
673
|
+
);
|
|
674
|
+
return void 0;
|
|
675
|
+
}
|
|
676
|
+
const k = this.key(desc);
|
|
677
|
+
const face = this.hb.createFace(existingBlob, 0);
|
|
678
|
+
const font = this.hb.createFont(face);
|
|
679
|
+
const upem = face.upem || 1e3;
|
|
680
|
+
font.setScale(upem, upem);
|
|
681
|
+
if (typeof font.setVariations === "function") {
|
|
682
|
+
try {
|
|
683
|
+
font.setVariations(`wght=${targetWeightNum}`);
|
|
684
|
+
} catch (e) {
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
this.faces.set(k, face);
|
|
688
|
+
this.fonts.set(k, font);
|
|
689
|
+
try {
|
|
690
|
+
const variationFont = baseFkFont.getVariation({ wght: targetWeightNum });
|
|
691
|
+
this.fontkitFonts.set(k, variationFont);
|
|
692
|
+
console.log(
|
|
693
|
+
`\u2705 Derived variable font: ${targetFamily} weight=${targetWeightNum} from existing registration`
|
|
694
|
+
);
|
|
695
|
+
} catch (err) {
|
|
696
|
+
this.fontkitFonts.set(k, baseFkFont);
|
|
697
|
+
console.warn(`\u26A0\uFE0F Could not apply weight variation, using base font for ${targetFamily}`);
|
|
698
|
+
}
|
|
699
|
+
return font;
|
|
700
|
+
}
|
|
589
701
|
async getFace(desc) {
|
|
590
702
|
try {
|
|
591
703
|
if (!this.hb) await this.init();
|
|
@@ -595,6 +707,10 @@ var _FontRegistry = class _FontRegistry {
|
|
|
595
707
|
const installed = await this.tryFallbackInstall(desc);
|
|
596
708
|
face = installed ? this.faces.get(k) : void 0;
|
|
597
709
|
}
|
|
710
|
+
if (!face) {
|
|
711
|
+
await this.tryDeriveFromExistingFont(desc);
|
|
712
|
+
face = this.faces.get(k);
|
|
713
|
+
}
|
|
598
714
|
return face;
|
|
599
715
|
} catch (err) {
|
|
600
716
|
throw new Error(
|
|
@@ -665,6 +781,9 @@ var _FontRegistry = class _FontRegistry {
|
|
|
665
781
|
this.faces.clear();
|
|
666
782
|
this.blobs.clear();
|
|
667
783
|
this.fontkitFonts.clear();
|
|
784
|
+
this.fontkitBaseFonts.clear();
|
|
785
|
+
this.colorEmojiFonts.clear();
|
|
786
|
+
this.colorEmojiFontBytes.clear();
|
|
668
787
|
this.hb = void 0;
|
|
669
788
|
this.initPromise = void 0;
|
|
670
789
|
} catch (err) {
|
|
@@ -794,7 +913,10 @@ var LayoutEngine = class {
|
|
|
794
913
|
const charIndex = g.cl;
|
|
795
914
|
let char;
|
|
796
915
|
if (charIndex >= 0 && charIndex < input.length) {
|
|
797
|
-
|
|
916
|
+
const codePoint = input.codePointAt(charIndex);
|
|
917
|
+
if (codePoint !== void 0) {
|
|
918
|
+
char = String.fromCodePoint(codePoint);
|
|
919
|
+
}
|
|
798
920
|
}
|
|
799
921
|
return {
|
|
800
922
|
id: g.g,
|
|
@@ -979,13 +1101,27 @@ async function buildDrawOps(p) {
|
|
|
979
1101
|
});
|
|
980
1102
|
}
|
|
981
1103
|
for (const glyph of line.glyphs) {
|
|
1104
|
+
const glyphX = xCursor + glyph.xOffset;
|
|
1105
|
+
const glyphY = baselineY + glyph.yOffset;
|
|
1106
|
+
const glyphFamily = glyph.fontDesc?.family;
|
|
1107
|
+
const isColorEmoji = glyph.isColorEmoji || glyphFamily && p.isColorEmojiFont && p.isColorEmojiFont(glyphFamily);
|
|
1108
|
+
if (isColorEmoji && glyph.char) {
|
|
1109
|
+
textOps.push({
|
|
1110
|
+
op: "DrawColorEmoji",
|
|
1111
|
+
char: glyph.char,
|
|
1112
|
+
x: glyphX,
|
|
1113
|
+
y: glyphY,
|
|
1114
|
+
fontSize: p.font.size,
|
|
1115
|
+
fontFamily: glyphFamily || "NotoColorEmoji"
|
|
1116
|
+
});
|
|
1117
|
+
xCursor += glyph.xAdvance;
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
982
1120
|
const path = await p.glyphPathProvider(glyph.id, glyph.fontDesc);
|
|
983
1121
|
if (!path || path === "M 0 0") {
|
|
984
1122
|
xCursor += glyph.xAdvance;
|
|
985
1123
|
continue;
|
|
986
1124
|
}
|
|
987
|
-
const glyphX = xCursor + glyph.xOffset;
|
|
988
|
-
const glyphY = baselineY + glyph.yOffset;
|
|
989
1125
|
const pb = computePathBounds(path);
|
|
990
1126
|
const x1 = glyphX + scale * pb.x;
|
|
991
1127
|
const x2 = glyphX + scale * (pb.x + pb.w);
|
|
@@ -1913,6 +2049,14 @@ function createWebPainter(canvas) {
|
|
|
1913
2049
|
ctx.restore();
|
|
1914
2050
|
continue;
|
|
1915
2051
|
}
|
|
2052
|
+
if (op.op === "DrawColorEmoji") {
|
|
2053
|
+
ctx.save();
|
|
2054
|
+
ctx.font = `${op.fontSize}px "${op.fontFamily}"`;
|
|
2055
|
+
ctx.textBaseline = "alphabetic";
|
|
2056
|
+
ctx.fillText(op.char, op.x, op.y);
|
|
2057
|
+
ctx.restore();
|
|
2058
|
+
continue;
|
|
2059
|
+
}
|
|
1916
2060
|
}
|
|
1917
2061
|
}
|
|
1918
2062
|
};
|
|
@@ -2213,12 +2357,17 @@ async function createTextEngine(opts = {}) {
|
|
|
2213
2357
|
const desc = { family: main.family, weight: `${main.weight}` };
|
|
2214
2358
|
let lines;
|
|
2215
2359
|
try {
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2360
|
+
let emojiFallback = void 0;
|
|
2361
|
+
const colorEmojiFamilies = fonts.getColorEmojiFontFamilies();
|
|
2362
|
+
if (colorEmojiFamilies.length > 0) {
|
|
2363
|
+
emojiFallback = { family: colorEmojiFamilies[0], weight: "400" };
|
|
2364
|
+
} else {
|
|
2365
|
+
const notoEmojiDesc = { family: "NotoEmoji", weight: "400" };
|
|
2366
|
+
try {
|
|
2367
|
+
await fonts.getFace(notoEmojiDesc);
|
|
2368
|
+
emojiFallback = notoEmojiDesc;
|
|
2369
|
+
} catch {
|
|
2370
|
+
}
|
|
2222
2371
|
}
|
|
2223
2372
|
const padding2 = asset.padding ? typeof asset.padding === "number" ? {
|
|
2224
2373
|
top: asset.padding,
|
|
@@ -2234,7 +2383,7 @@ async function createTextEngine(opts = {}) {
|
|
|
2234
2383
|
lineHeight: asset.style?.lineHeight ?? 1.2,
|
|
2235
2384
|
desc,
|
|
2236
2385
|
textTransform: asset.style?.textTransform ?? "none",
|
|
2237
|
-
emojiFallback
|
|
2386
|
+
emojiFallback
|
|
2238
2387
|
});
|
|
2239
2388
|
} catch (err) {
|
|
2240
2389
|
throw new Error(
|
|
@@ -2289,7 +2438,8 @@ async function createTextEngine(opts = {}) {
|
|
|
2289
2438
|
border: asset.border,
|
|
2290
2439
|
padding: asset.padding,
|
|
2291
2440
|
glyphPathProvider: (gid, fontDesc) => fonts.glyphPath(fontDesc || desc, gid),
|
|
2292
|
-
getUnitsPerEm: (fontDesc) => fonts.getUnitsPerEm(fontDesc || desc)
|
|
2441
|
+
getUnitsPerEm: (fontDesc) => fonts.getUnitsPerEm(fontDesc || desc),
|
|
2442
|
+
isColorEmojiFont: (family) => fonts.isColorEmojiFont(family)
|
|
2293
2443
|
});
|
|
2294
2444
|
} catch (err) {
|
|
2295
2445
|
throw new Error(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shotstack/shotstack-canvas",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
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",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
},
|
|
43
43
|
"sideEffects": false,
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@shotstack/schemas": "^1.3.
|
|
45
|
+
"@shotstack/schemas": "^1.3.1",
|
|
46
46
|
"canvas": "npm:@napi-rs/canvas@^0.1.54",
|
|
47
47
|
"ffmpeg-static": "^5.2.0",
|
|
48
48
|
"fontkit": "^2.0.4",
|