@shotstack/shotstack-canvas 2.0.14 → 2.0.16
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/README.md +72 -13
- package/dist/entry.node.cjs +98 -41
- package/dist/entry.node.d.cts +1 -0
- package/dist/entry.node.d.ts +1 -0
- package/dist/entry.node.js +98 -41
- package/dist/entry.web.d.ts +1 -0
- package/dist/entry.web.js +97 -41
- package/package.json +78 -66
- package/scripts/postinstall.js +58 -58
package/README.md
CHANGED
|
@@ -1,13 +1,72 @@
|
|
|
1
|
-
# @shotstack/shotstack-canvas
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
1
|
+
# @shotstack/shotstack-canvas
|
|
2
|
+
|
|
3
|
+
Text layout and animation engine using HarfBuzz WASM. Identical rendering on Node.js and Web.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- HarfBuzz WASM text shaping with glyph outlines
|
|
8
|
+
- Cross-platform DrawOp abstraction (Canvas2D for web, @napi-rs/canvas for Node.js)
|
|
9
|
+
- Rich text with fonts, stroke, shadow, gradients, backgrounds, and borders
|
|
10
|
+
- SVG asset rendering (11 shape types + SVG markup import)
|
|
11
|
+
- Rich caption support with word-level timing and 8 animation styles
|
|
12
|
+
- Video generation (H.264 MP4 via FFmpeg on Node.js, WebCodecs in browser)
|
|
13
|
+
- RTL and bidirectional text support
|
|
14
|
+
- Custom font registration (file, URL, or ArrayBuffer)
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pnpm add @shotstack/shotstack-canvas
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
### Node.js
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
import { createTextEngine } from "@shotstack/shotstack-canvas";
|
|
28
|
+
|
|
29
|
+
const engine = await createTextEngine({ width: 800, height: 400 });
|
|
30
|
+
|
|
31
|
+
const asset = engine.validate({
|
|
32
|
+
type: "rich-text",
|
|
33
|
+
text: "Hello World",
|
|
34
|
+
font: { family: "Open Sans", size: 48, color: "#ffffff" },
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const drawOps = await engine.renderFrame(asset.value, 0);
|
|
38
|
+
const renderer = await engine.createRenderer({ width: 800, height: 400 });
|
|
39
|
+
const buffer = renderer.paint(drawOps);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Web
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
import { createTextEngine } from "@shotstack/shotstack-canvas";
|
|
46
|
+
|
|
47
|
+
const engine = await createTextEngine({ width: 800, height: 400 });
|
|
48
|
+
const asset = engine.validate({ type: "rich-text", text: "Hello World" });
|
|
49
|
+
const drawOps = await engine.renderFrame(asset.value, 0);
|
|
50
|
+
const renderer = engine.createRenderer(canvas);
|
|
51
|
+
renderer.paint(drawOps);
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Development
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pnpm install
|
|
58
|
+
pnpm build
|
|
59
|
+
pnpm test
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Release
|
|
63
|
+
|
|
64
|
+
Releases are automated via [semantic-release](https://github.com/semantic-release/semantic-release). Pushing to `main` with Conventional Commit messages triggers a release:
|
|
65
|
+
|
|
66
|
+
- `fix: ...` — patch release (e.g., 2.0.15 → 2.0.16)
|
|
67
|
+
- `feat: ...` — minor release (e.g., 2.0.15 → 2.1.0)
|
|
68
|
+
- `feat!: ...` or `BREAKING CHANGE:` — major release (e.g., 2.0.15 → 3.0.0)
|
|
69
|
+
|
|
70
|
+
## License
|
|
71
|
+
|
|
72
|
+
MIT
|
package/dist/entry.node.cjs
CHANGED
|
@@ -1377,17 +1377,45 @@ function mirrorAnimationDirection(direction, isRTL) {
|
|
|
1377
1377
|
}
|
|
1378
1378
|
|
|
1379
1379
|
// src/core/layout.ts
|
|
1380
|
+
var import_bidi_js2 = __toESM(require("bidi-js"), 1);
|
|
1380
1381
|
function isEmoji(char) {
|
|
1381
1382
|
const code = char.codePointAt(0);
|
|
1382
1383
|
if (!code) return false;
|
|
1383
|
-
return code >= 127744 && code <= 129535 ||
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1384
|
+
return code >= 127744 && code <= 129535 || code >= 9728 && code <= 9983 || code >= 9984 && code <= 10175 || code >= 65024 && code <= 65039 || code >= 128512 && code <= 128591 || code >= 128640 && code <= 128767 || code >= 129280 && code <= 129535 || code >= 129648 && code <= 129791;
|
|
1385
|
+
}
|
|
1386
|
+
function reorderRunsVisually(runs) {
|
|
1387
|
+
if (runs.length <= 1) return runs;
|
|
1388
|
+
const result = runs.slice();
|
|
1389
|
+
const runLevels = result.map((r) => r.level);
|
|
1390
|
+
const maxLevel = Math.max(...runLevels);
|
|
1391
|
+
const minLevel = Math.min(...runLevels);
|
|
1392
|
+
const minOddLevel = minLevel % 2 === 1 ? minLevel : minLevel + 1;
|
|
1393
|
+
for (let level = maxLevel; level >= minOddLevel; level--) {
|
|
1394
|
+
let start = 0;
|
|
1395
|
+
while (start < result.length) {
|
|
1396
|
+
if (runLevels[result.indexOf(result[start])] >= level) {
|
|
1397
|
+
let end = start;
|
|
1398
|
+
while (end + 1 < result.length && result[end + 1].level >= level) {
|
|
1399
|
+
end++;
|
|
1400
|
+
}
|
|
1401
|
+
if (start < end) {
|
|
1402
|
+
let left = start;
|
|
1403
|
+
let right = end;
|
|
1404
|
+
while (left < right) {
|
|
1405
|
+
const tmp = result[left];
|
|
1406
|
+
result[left] = result[right];
|
|
1407
|
+
result[right] = tmp;
|
|
1408
|
+
left++;
|
|
1409
|
+
right--;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
start = end + 1;
|
|
1413
|
+
} else {
|
|
1414
|
+
start++;
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
return result;
|
|
1391
1419
|
}
|
|
1392
1420
|
var LayoutEngine = class {
|
|
1393
1421
|
constructor(fonts) {
|
|
@@ -1412,6 +1440,9 @@ var LayoutEngine = class {
|
|
|
1412
1440
|
try {
|
|
1413
1441
|
buffer.addText(text);
|
|
1414
1442
|
buffer.guessSegmentProperties();
|
|
1443
|
+
if (direction) {
|
|
1444
|
+
buffer.setDirection(direction);
|
|
1445
|
+
}
|
|
1415
1446
|
const font = await this.fonts.getFont(desc);
|
|
1416
1447
|
const face = await this.fonts.getFace(desc);
|
|
1417
1448
|
const upem = face?.upem || 1e3;
|
|
@@ -1434,6 +1465,64 @@ var LayoutEngine = class {
|
|
|
1434
1465
|
);
|
|
1435
1466
|
}
|
|
1436
1467
|
}
|
|
1468
|
+
splitIntoBidiRuns(input, levels, desc, emojiFallback) {
|
|
1469
|
+
const runs = [];
|
|
1470
|
+
if (input.length === 0) return runs;
|
|
1471
|
+
let runStart = 0;
|
|
1472
|
+
let runLevel = levels[0];
|
|
1473
|
+
let runIsEmoji = emojiFallback ? isEmoji(input[0]) : false;
|
|
1474
|
+
for (let i = 1; i <= input.length; i++) {
|
|
1475
|
+
const atEnd = i === input.length;
|
|
1476
|
+
const charLevel = atEnd ? -1 : levels[i];
|
|
1477
|
+
const charIsEmoji = !atEnd && emojiFallback ? isEmoji(String.fromCodePoint(input.codePointAt(i) ?? 0)) : false;
|
|
1478
|
+
if (atEnd || charLevel !== runLevel || emojiFallback && charIsEmoji !== runIsEmoji) {
|
|
1479
|
+
const text = input.slice(runStart, i);
|
|
1480
|
+
const font = runIsEmoji && emojiFallback ? emojiFallback : desc;
|
|
1481
|
+
runs.push({ text, startIndex: runStart, endIndex: i, level: runLevel, font });
|
|
1482
|
+
if (!atEnd) {
|
|
1483
|
+
runStart = i;
|
|
1484
|
+
runLevel = charLevel;
|
|
1485
|
+
runIsEmoji = charIsEmoji;
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
return runs;
|
|
1490
|
+
}
|
|
1491
|
+
async shapeWithBidi(input, desc, emojiFallback) {
|
|
1492
|
+
const hasRTL = containsRTLCharacters(input);
|
|
1493
|
+
const hasLTR = /[a-zA-Z0-9]/.test(input);
|
|
1494
|
+
const hasMixedDirection = hasRTL && hasLTR;
|
|
1495
|
+
if (!hasMixedDirection && !emojiFallback) {
|
|
1496
|
+
const textDirection = hasRTL ? "rtl" : void 0;
|
|
1497
|
+
return this.shapeFull(input, desc, textDirection);
|
|
1498
|
+
}
|
|
1499
|
+
const bidi = (0, import_bidi_js2.default)();
|
|
1500
|
+
const { levels } = bidi.getEmbeddingLevels(input);
|
|
1501
|
+
const bidiRuns = this.splitIntoBidiRuns(input, levels, desc, emojiFallback);
|
|
1502
|
+
const shapedRuns = [];
|
|
1503
|
+
for (const run of bidiRuns) {
|
|
1504
|
+
const runDirection = run.level % 2 === 1 ? "rtl" : "ltr";
|
|
1505
|
+
const runShaped = await this.shapeFull(run.text, run.font, runDirection);
|
|
1506
|
+
for (const glyph of runShaped) {
|
|
1507
|
+
glyph.cl += run.startIndex;
|
|
1508
|
+
if (run.font !== desc) {
|
|
1509
|
+
glyph.fontDesc = run.font;
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
shapedRuns.push({
|
|
1513
|
+
glyphs: runShaped,
|
|
1514
|
+
startIndex: run.startIndex,
|
|
1515
|
+
endIndex: run.endIndex,
|
|
1516
|
+
level: run.level
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
const visualRuns = reorderRunsVisually(shapedRuns);
|
|
1520
|
+
const visualGlyphs = [];
|
|
1521
|
+
for (const run of visualRuns) {
|
|
1522
|
+
visualGlyphs.push(...run.glyphs);
|
|
1523
|
+
}
|
|
1524
|
+
return visualGlyphs;
|
|
1525
|
+
}
|
|
1437
1526
|
async layout(params) {
|
|
1438
1527
|
try {
|
|
1439
1528
|
const { textTransform, desc, fontSize, letterSpacing, width, emojiFallback } = params;
|
|
@@ -1443,38 +1532,7 @@ var LayoutEngine = class {
|
|
|
1443
1532
|
}
|
|
1444
1533
|
let shaped;
|
|
1445
1534
|
try {
|
|
1446
|
-
|
|
1447
|
-
const textDirection = containsRTLCharacters(input) ? "rtl" : void 0;
|
|
1448
|
-
shaped = await this.shapeFull(input, desc, textDirection);
|
|
1449
|
-
} else {
|
|
1450
|
-
const chars = Array.from(input);
|
|
1451
|
-
const runs = [];
|
|
1452
|
-
let currentRun = { text: "", startIndex: 0, isEmoji: false };
|
|
1453
|
-
for (let i = 0; i < chars.length; i++) {
|
|
1454
|
-
const char = chars[i];
|
|
1455
|
-
const charIsEmoji = isEmoji(char);
|
|
1456
|
-
if (i === 0) {
|
|
1457
|
-
currentRun = { text: char, startIndex: 0, isEmoji: charIsEmoji };
|
|
1458
|
-
} else if (currentRun.isEmoji === charIsEmoji) {
|
|
1459
|
-
currentRun.text += char;
|
|
1460
|
-
} else {
|
|
1461
|
-
runs.push(currentRun);
|
|
1462
|
-
currentRun = { text: char, startIndex: currentRun.startIndex + currentRun.text.length, isEmoji: charIsEmoji };
|
|
1463
|
-
}
|
|
1464
|
-
}
|
|
1465
|
-
if (currentRun.text) runs.push(currentRun);
|
|
1466
|
-
shaped = [];
|
|
1467
|
-
for (const run of runs) {
|
|
1468
|
-
const runFont = run.isEmoji ? emojiFallback : desc;
|
|
1469
|
-
const runDirection = containsRTLCharacters(run.text) ? "rtl" : void 0;
|
|
1470
|
-
const runShaped = await this.shapeFull(run.text, runFont, runDirection);
|
|
1471
|
-
for (const glyph of runShaped) {
|
|
1472
|
-
glyph.cl += run.startIndex;
|
|
1473
|
-
glyph.fontDesc = runFont;
|
|
1474
|
-
}
|
|
1475
|
-
shaped.push(...runShaped);
|
|
1476
|
-
}
|
|
1477
|
-
}
|
|
1535
|
+
shaped = await this.shapeWithBidi(input, desc, emojiFallback);
|
|
1478
1536
|
} catch (err) {
|
|
1479
1537
|
throw new Error(`Text shaping failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1480
1538
|
}
|
|
@@ -1505,7 +1563,6 @@ var LayoutEngine = class {
|
|
|
1505
1563
|
cluster: g.cl,
|
|
1506
1564
|
char,
|
|
1507
1565
|
fontDesc: g.fontDesc
|
|
1508
|
-
// Preserve font descriptor
|
|
1509
1566
|
};
|
|
1510
1567
|
});
|
|
1511
1568
|
const lines = [];
|
package/dist/entry.node.d.cts
CHANGED
package/dist/entry.node.d.ts
CHANGED
package/dist/entry.node.js
CHANGED
|
@@ -974,17 +974,45 @@ function mirrorAnimationDirection(direction, isRTL) {
|
|
|
974
974
|
}
|
|
975
975
|
|
|
976
976
|
// src/core/layout.ts
|
|
977
|
+
import bidiFactory2 from "bidi-js";
|
|
977
978
|
function isEmoji(char) {
|
|
978
979
|
const code = char.codePointAt(0);
|
|
979
980
|
if (!code) return false;
|
|
980
|
-
return code >= 127744 && code <= 129535 ||
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
981
|
+
return code >= 127744 && code <= 129535 || code >= 9728 && code <= 9983 || code >= 9984 && code <= 10175 || code >= 65024 && code <= 65039 || code >= 128512 && code <= 128591 || code >= 128640 && code <= 128767 || code >= 129280 && code <= 129535 || code >= 129648 && code <= 129791;
|
|
982
|
+
}
|
|
983
|
+
function reorderRunsVisually(runs) {
|
|
984
|
+
if (runs.length <= 1) return runs;
|
|
985
|
+
const result = runs.slice();
|
|
986
|
+
const runLevels = result.map((r) => r.level);
|
|
987
|
+
const maxLevel = Math.max(...runLevels);
|
|
988
|
+
const minLevel = Math.min(...runLevels);
|
|
989
|
+
const minOddLevel = minLevel % 2 === 1 ? minLevel : minLevel + 1;
|
|
990
|
+
for (let level = maxLevel; level >= minOddLevel; level--) {
|
|
991
|
+
let start = 0;
|
|
992
|
+
while (start < result.length) {
|
|
993
|
+
if (runLevels[result.indexOf(result[start])] >= level) {
|
|
994
|
+
let end = start;
|
|
995
|
+
while (end + 1 < result.length && result[end + 1].level >= level) {
|
|
996
|
+
end++;
|
|
997
|
+
}
|
|
998
|
+
if (start < end) {
|
|
999
|
+
let left = start;
|
|
1000
|
+
let right = end;
|
|
1001
|
+
while (left < right) {
|
|
1002
|
+
const tmp = result[left];
|
|
1003
|
+
result[left] = result[right];
|
|
1004
|
+
result[right] = tmp;
|
|
1005
|
+
left++;
|
|
1006
|
+
right--;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
start = end + 1;
|
|
1010
|
+
} else {
|
|
1011
|
+
start++;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
return result;
|
|
988
1016
|
}
|
|
989
1017
|
var LayoutEngine = class {
|
|
990
1018
|
constructor(fonts) {
|
|
@@ -1009,6 +1037,9 @@ var LayoutEngine = class {
|
|
|
1009
1037
|
try {
|
|
1010
1038
|
buffer.addText(text);
|
|
1011
1039
|
buffer.guessSegmentProperties();
|
|
1040
|
+
if (direction) {
|
|
1041
|
+
buffer.setDirection(direction);
|
|
1042
|
+
}
|
|
1012
1043
|
const font = await this.fonts.getFont(desc);
|
|
1013
1044
|
const face = await this.fonts.getFace(desc);
|
|
1014
1045
|
const upem = face?.upem || 1e3;
|
|
@@ -1031,6 +1062,64 @@ var LayoutEngine = class {
|
|
|
1031
1062
|
);
|
|
1032
1063
|
}
|
|
1033
1064
|
}
|
|
1065
|
+
splitIntoBidiRuns(input, levels, desc, emojiFallback) {
|
|
1066
|
+
const runs = [];
|
|
1067
|
+
if (input.length === 0) return runs;
|
|
1068
|
+
let runStart = 0;
|
|
1069
|
+
let runLevel = levels[0];
|
|
1070
|
+
let runIsEmoji = emojiFallback ? isEmoji(input[0]) : false;
|
|
1071
|
+
for (let i = 1; i <= input.length; i++) {
|
|
1072
|
+
const atEnd = i === input.length;
|
|
1073
|
+
const charLevel = atEnd ? -1 : levels[i];
|
|
1074
|
+
const charIsEmoji = !atEnd && emojiFallback ? isEmoji(String.fromCodePoint(input.codePointAt(i) ?? 0)) : false;
|
|
1075
|
+
if (atEnd || charLevel !== runLevel || emojiFallback && charIsEmoji !== runIsEmoji) {
|
|
1076
|
+
const text = input.slice(runStart, i);
|
|
1077
|
+
const font = runIsEmoji && emojiFallback ? emojiFallback : desc;
|
|
1078
|
+
runs.push({ text, startIndex: runStart, endIndex: i, level: runLevel, font });
|
|
1079
|
+
if (!atEnd) {
|
|
1080
|
+
runStart = i;
|
|
1081
|
+
runLevel = charLevel;
|
|
1082
|
+
runIsEmoji = charIsEmoji;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
return runs;
|
|
1087
|
+
}
|
|
1088
|
+
async shapeWithBidi(input, desc, emojiFallback) {
|
|
1089
|
+
const hasRTL = containsRTLCharacters(input);
|
|
1090
|
+
const hasLTR = /[a-zA-Z0-9]/.test(input);
|
|
1091
|
+
const hasMixedDirection = hasRTL && hasLTR;
|
|
1092
|
+
if (!hasMixedDirection && !emojiFallback) {
|
|
1093
|
+
const textDirection = hasRTL ? "rtl" : void 0;
|
|
1094
|
+
return this.shapeFull(input, desc, textDirection);
|
|
1095
|
+
}
|
|
1096
|
+
const bidi = bidiFactory2();
|
|
1097
|
+
const { levels } = bidi.getEmbeddingLevels(input);
|
|
1098
|
+
const bidiRuns = this.splitIntoBidiRuns(input, levels, desc, emojiFallback);
|
|
1099
|
+
const shapedRuns = [];
|
|
1100
|
+
for (const run of bidiRuns) {
|
|
1101
|
+
const runDirection = run.level % 2 === 1 ? "rtl" : "ltr";
|
|
1102
|
+
const runShaped = await this.shapeFull(run.text, run.font, runDirection);
|
|
1103
|
+
for (const glyph of runShaped) {
|
|
1104
|
+
glyph.cl += run.startIndex;
|
|
1105
|
+
if (run.font !== desc) {
|
|
1106
|
+
glyph.fontDesc = run.font;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
shapedRuns.push({
|
|
1110
|
+
glyphs: runShaped,
|
|
1111
|
+
startIndex: run.startIndex,
|
|
1112
|
+
endIndex: run.endIndex,
|
|
1113
|
+
level: run.level
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
const visualRuns = reorderRunsVisually(shapedRuns);
|
|
1117
|
+
const visualGlyphs = [];
|
|
1118
|
+
for (const run of visualRuns) {
|
|
1119
|
+
visualGlyphs.push(...run.glyphs);
|
|
1120
|
+
}
|
|
1121
|
+
return visualGlyphs;
|
|
1122
|
+
}
|
|
1034
1123
|
async layout(params) {
|
|
1035
1124
|
try {
|
|
1036
1125
|
const { textTransform, desc, fontSize, letterSpacing, width, emojiFallback } = params;
|
|
@@ -1040,38 +1129,7 @@ var LayoutEngine = class {
|
|
|
1040
1129
|
}
|
|
1041
1130
|
let shaped;
|
|
1042
1131
|
try {
|
|
1043
|
-
|
|
1044
|
-
const textDirection = containsRTLCharacters(input) ? "rtl" : void 0;
|
|
1045
|
-
shaped = await this.shapeFull(input, desc, textDirection);
|
|
1046
|
-
} else {
|
|
1047
|
-
const chars = Array.from(input);
|
|
1048
|
-
const runs = [];
|
|
1049
|
-
let currentRun = { text: "", startIndex: 0, isEmoji: false };
|
|
1050
|
-
for (let i = 0; i < chars.length; i++) {
|
|
1051
|
-
const char = chars[i];
|
|
1052
|
-
const charIsEmoji = isEmoji(char);
|
|
1053
|
-
if (i === 0) {
|
|
1054
|
-
currentRun = { text: char, startIndex: 0, isEmoji: charIsEmoji };
|
|
1055
|
-
} else if (currentRun.isEmoji === charIsEmoji) {
|
|
1056
|
-
currentRun.text += char;
|
|
1057
|
-
} else {
|
|
1058
|
-
runs.push(currentRun);
|
|
1059
|
-
currentRun = { text: char, startIndex: currentRun.startIndex + currentRun.text.length, isEmoji: charIsEmoji };
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
if (currentRun.text) runs.push(currentRun);
|
|
1063
|
-
shaped = [];
|
|
1064
|
-
for (const run of runs) {
|
|
1065
|
-
const runFont = run.isEmoji ? emojiFallback : desc;
|
|
1066
|
-
const runDirection = containsRTLCharacters(run.text) ? "rtl" : void 0;
|
|
1067
|
-
const runShaped = await this.shapeFull(run.text, runFont, runDirection);
|
|
1068
|
-
for (const glyph of runShaped) {
|
|
1069
|
-
glyph.cl += run.startIndex;
|
|
1070
|
-
glyph.fontDesc = runFont;
|
|
1071
|
-
}
|
|
1072
|
-
shaped.push(...runShaped);
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1132
|
+
shaped = await this.shapeWithBidi(input, desc, emojiFallback);
|
|
1075
1133
|
} catch (err) {
|
|
1076
1134
|
throw new Error(`Text shaping failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1077
1135
|
}
|
|
@@ -1102,7 +1160,6 @@ var LayoutEngine = class {
|
|
|
1102
1160
|
cluster: g.cl,
|
|
1103
1161
|
char,
|
|
1104
1162
|
fontDesc: g.fontDesc
|
|
1105
|
-
// Preserve font descriptor
|
|
1106
1163
|
};
|
|
1107
1164
|
});
|
|
1108
1165
|
const lines = [];
|
package/dist/entry.web.d.ts
CHANGED
package/dist/entry.web.js
CHANGED
|
@@ -33016,14 +33016,41 @@ function mirrorAnimationDirection(direction, isRTL) {
|
|
|
33016
33016
|
function isEmoji(char) {
|
|
33017
33017
|
const code = char.codePointAt(0);
|
|
33018
33018
|
if (!code) return false;
|
|
33019
|
-
return code >= 127744 && code <= 129535 ||
|
|
33020
|
-
|
|
33021
|
-
|
|
33022
|
-
|
|
33023
|
-
|
|
33024
|
-
|
|
33025
|
-
|
|
33026
|
-
|
|
33019
|
+
return code >= 127744 && code <= 129535 || code >= 9728 && code <= 9983 || code >= 9984 && code <= 10175 || code >= 65024 && code <= 65039 || code >= 128512 && code <= 128591 || code >= 128640 && code <= 128767 || code >= 129280 && code <= 129535 || code >= 129648 && code <= 129791;
|
|
33020
|
+
}
|
|
33021
|
+
function reorderRunsVisually(runs) {
|
|
33022
|
+
if (runs.length <= 1) return runs;
|
|
33023
|
+
const result = runs.slice();
|
|
33024
|
+
const runLevels = result.map((r) => r.level);
|
|
33025
|
+
const maxLevel = Math.max(...runLevels);
|
|
33026
|
+
const minLevel = Math.min(...runLevels);
|
|
33027
|
+
const minOddLevel = minLevel % 2 === 1 ? minLevel : minLevel + 1;
|
|
33028
|
+
for (let level = maxLevel; level >= minOddLevel; level--) {
|
|
33029
|
+
let start = 0;
|
|
33030
|
+
while (start < result.length) {
|
|
33031
|
+
if (runLevels[result.indexOf(result[start])] >= level) {
|
|
33032
|
+
let end = start;
|
|
33033
|
+
while (end + 1 < result.length && result[end + 1].level >= level) {
|
|
33034
|
+
end++;
|
|
33035
|
+
}
|
|
33036
|
+
if (start < end) {
|
|
33037
|
+
let left = start;
|
|
33038
|
+
let right = end;
|
|
33039
|
+
while (left < right) {
|
|
33040
|
+
const tmp = result[left];
|
|
33041
|
+
result[left] = result[right];
|
|
33042
|
+
result[right] = tmp;
|
|
33043
|
+
left++;
|
|
33044
|
+
right--;
|
|
33045
|
+
}
|
|
33046
|
+
}
|
|
33047
|
+
start = end + 1;
|
|
33048
|
+
} else {
|
|
33049
|
+
start++;
|
|
33050
|
+
}
|
|
33051
|
+
}
|
|
33052
|
+
}
|
|
33053
|
+
return result;
|
|
33027
33054
|
}
|
|
33028
33055
|
var LayoutEngine = class {
|
|
33029
33056
|
constructor(fonts) {
|
|
@@ -33048,6 +33075,9 @@ var LayoutEngine = class {
|
|
|
33048
33075
|
try {
|
|
33049
33076
|
buffer.addText(text);
|
|
33050
33077
|
buffer.guessSegmentProperties();
|
|
33078
|
+
if (direction) {
|
|
33079
|
+
buffer.setDirection(direction);
|
|
33080
|
+
}
|
|
33051
33081
|
const font = await this.fonts.getFont(desc);
|
|
33052
33082
|
const face = await this.fonts.getFace(desc);
|
|
33053
33083
|
const upem = face?.upem || 1e3;
|
|
@@ -33070,6 +33100,64 @@ var LayoutEngine = class {
|
|
|
33070
33100
|
);
|
|
33071
33101
|
}
|
|
33072
33102
|
}
|
|
33103
|
+
splitIntoBidiRuns(input, levels, desc, emojiFallback) {
|
|
33104
|
+
const runs = [];
|
|
33105
|
+
if (input.length === 0) return runs;
|
|
33106
|
+
let runStart = 0;
|
|
33107
|
+
let runLevel = levels[0];
|
|
33108
|
+
let runIsEmoji = emojiFallback ? isEmoji(input[0]) : false;
|
|
33109
|
+
for (let i = 1; i <= input.length; i++) {
|
|
33110
|
+
const atEnd = i === input.length;
|
|
33111
|
+
const charLevel = atEnd ? -1 : levels[i];
|
|
33112
|
+
const charIsEmoji = !atEnd && emojiFallback ? isEmoji(String.fromCodePoint(input.codePointAt(i) ?? 0)) : false;
|
|
33113
|
+
if (atEnd || charLevel !== runLevel || emojiFallback && charIsEmoji !== runIsEmoji) {
|
|
33114
|
+
const text = input.slice(runStart, i);
|
|
33115
|
+
const font = runIsEmoji && emojiFallback ? emojiFallback : desc;
|
|
33116
|
+
runs.push({ text, startIndex: runStart, endIndex: i, level: runLevel, font });
|
|
33117
|
+
if (!atEnd) {
|
|
33118
|
+
runStart = i;
|
|
33119
|
+
runLevel = charLevel;
|
|
33120
|
+
runIsEmoji = charIsEmoji;
|
|
33121
|
+
}
|
|
33122
|
+
}
|
|
33123
|
+
}
|
|
33124
|
+
return runs;
|
|
33125
|
+
}
|
|
33126
|
+
async shapeWithBidi(input, desc, emojiFallback) {
|
|
33127
|
+
const hasRTL = containsRTLCharacters(input);
|
|
33128
|
+
const hasLTR = /[a-zA-Z0-9]/.test(input);
|
|
33129
|
+
const hasMixedDirection = hasRTL && hasLTR;
|
|
33130
|
+
if (!hasMixedDirection && !emojiFallback) {
|
|
33131
|
+
const textDirection = hasRTL ? "rtl" : void 0;
|
|
33132
|
+
return this.shapeFull(input, desc, textDirection);
|
|
33133
|
+
}
|
|
33134
|
+
const bidi = bidi_default();
|
|
33135
|
+
const { levels } = bidi.getEmbeddingLevels(input);
|
|
33136
|
+
const bidiRuns = this.splitIntoBidiRuns(input, levels, desc, emojiFallback);
|
|
33137
|
+
const shapedRuns = [];
|
|
33138
|
+
for (const run of bidiRuns) {
|
|
33139
|
+
const runDirection = run.level % 2 === 1 ? "rtl" : "ltr";
|
|
33140
|
+
const runShaped = await this.shapeFull(run.text, run.font, runDirection);
|
|
33141
|
+
for (const glyph of runShaped) {
|
|
33142
|
+
glyph.cl += run.startIndex;
|
|
33143
|
+
if (run.font !== desc) {
|
|
33144
|
+
glyph.fontDesc = run.font;
|
|
33145
|
+
}
|
|
33146
|
+
}
|
|
33147
|
+
shapedRuns.push({
|
|
33148
|
+
glyphs: runShaped,
|
|
33149
|
+
startIndex: run.startIndex,
|
|
33150
|
+
endIndex: run.endIndex,
|
|
33151
|
+
level: run.level
|
|
33152
|
+
});
|
|
33153
|
+
}
|
|
33154
|
+
const visualRuns = reorderRunsVisually(shapedRuns);
|
|
33155
|
+
const visualGlyphs = [];
|
|
33156
|
+
for (const run of visualRuns) {
|
|
33157
|
+
visualGlyphs.push(...run.glyphs);
|
|
33158
|
+
}
|
|
33159
|
+
return visualGlyphs;
|
|
33160
|
+
}
|
|
33073
33161
|
async layout(params) {
|
|
33074
33162
|
try {
|
|
33075
33163
|
const { textTransform, desc, fontSize, letterSpacing, width, emojiFallback } = params;
|
|
@@ -33079,38 +33167,7 @@ var LayoutEngine = class {
|
|
|
33079
33167
|
}
|
|
33080
33168
|
let shaped;
|
|
33081
33169
|
try {
|
|
33082
|
-
|
|
33083
|
-
const textDirection = containsRTLCharacters(input) ? "rtl" : void 0;
|
|
33084
|
-
shaped = await this.shapeFull(input, desc, textDirection);
|
|
33085
|
-
} else {
|
|
33086
|
-
const chars = Array.from(input);
|
|
33087
|
-
const runs = [];
|
|
33088
|
-
let currentRun = { text: "", startIndex: 0, isEmoji: false };
|
|
33089
|
-
for (let i = 0; i < chars.length; i++) {
|
|
33090
|
-
const char = chars[i];
|
|
33091
|
-
const charIsEmoji = isEmoji(char);
|
|
33092
|
-
if (i === 0) {
|
|
33093
|
-
currentRun = { text: char, startIndex: 0, isEmoji: charIsEmoji };
|
|
33094
|
-
} else if (currentRun.isEmoji === charIsEmoji) {
|
|
33095
|
-
currentRun.text += char;
|
|
33096
|
-
} else {
|
|
33097
|
-
runs.push(currentRun);
|
|
33098
|
-
currentRun = { text: char, startIndex: currentRun.startIndex + currentRun.text.length, isEmoji: charIsEmoji };
|
|
33099
|
-
}
|
|
33100
|
-
}
|
|
33101
|
-
if (currentRun.text) runs.push(currentRun);
|
|
33102
|
-
shaped = [];
|
|
33103
|
-
for (const run of runs) {
|
|
33104
|
-
const runFont = run.isEmoji ? emojiFallback : desc;
|
|
33105
|
-
const runDirection = containsRTLCharacters(run.text) ? "rtl" : void 0;
|
|
33106
|
-
const runShaped = await this.shapeFull(run.text, runFont, runDirection);
|
|
33107
|
-
for (const glyph of runShaped) {
|
|
33108
|
-
glyph.cl += run.startIndex;
|
|
33109
|
-
glyph.fontDesc = runFont;
|
|
33110
|
-
}
|
|
33111
|
-
shaped.push(...runShaped);
|
|
33112
|
-
}
|
|
33113
|
-
}
|
|
33170
|
+
shaped = await this.shapeWithBidi(input, desc, emojiFallback);
|
|
33114
33171
|
} catch (err) {
|
|
33115
33172
|
throw new Error(`Text shaping failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
33116
33173
|
}
|
|
@@ -33141,7 +33198,6 @@ var LayoutEngine = class {
|
|
|
33141
33198
|
cluster: g.cl,
|
|
33142
33199
|
char,
|
|
33143
33200
|
fontDesc: g.fontDesc
|
|
33144
|
-
// Preserve font descriptor
|
|
33145
33201
|
};
|
|
33146
33202
|
});
|
|
33147
33203
|
const lines = [];
|
package/package.json
CHANGED
|
@@ -1,66 +1,78 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@shotstack/shotstack-canvas",
|
|
3
|
-
"version": "2.0.
|
|
4
|
-
"description": "Text layout & animation engine (HarfBuzz) for Node & Web - fully self-contained.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "./dist/entry.node.cjs",
|
|
7
|
-
"module": "./dist/entry.node.js",
|
|
8
|
-
"browser": "./dist/entry.web.js",
|
|
9
|
-
"types": "./dist/entry.node.d.ts",
|
|
10
|
-
"exports": {
|
|
11
|
-
".": {
|
|
12
|
-
"node": {
|
|
13
|
-
"import": "./dist/entry.node.js",
|
|
14
|
-
"require": "./dist/entry.node.cjs"
|
|
15
|
-
},
|
|
16
|
-
"browser": "./dist/entry.web.js",
|
|
17
|
-
"default": "./dist/entry.web.js"
|
|
18
|
-
}
|
|
19
|
-
},
|
|
20
|
-
"files": [
|
|
21
|
-
"dist/**",
|
|
22
|
-
"scripts/postinstall.js",
|
|
23
|
-
"README.md",
|
|
24
|
-
"LICENSE"
|
|
25
|
-
],
|
|
26
|
-
"scripts": {
|
|
27
|
-
"dev": "tsup --watch",
|
|
28
|
-
"build": "tsup",
|
|
29
|
-
"postinstall": "node scripts/postinstall.js",
|
|
30
|
-
"vendor:harfbuzz": "node scripts/vendor-harfbuzz.js",
|
|
31
|
-
"example:node": "node examples/node-example.mjs",
|
|
32
|
-
"example:video": "node examples/node-video.mjs",
|
|
33
|
-
"example:web": "vite dev examples/web-example",
|
|
34
|
-
"test:caption-web": "vite dev examples/caption-tests",
|
|
35
|
-
"prepublishOnly": "
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
"@resvg/resvg-
|
|
48
|
-
"
|
|
49
|
-
"@shotstack/schemas": "1.8.7",
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
|
|
66
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@shotstack/shotstack-canvas",
|
|
3
|
+
"version": "2.0.16",
|
|
4
|
+
"description": "Text layout & animation engine (HarfBuzz) for Node & Web - fully self-contained.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/entry.node.cjs",
|
|
7
|
+
"module": "./dist/entry.node.js",
|
|
8
|
+
"browser": "./dist/entry.web.js",
|
|
9
|
+
"types": "./dist/entry.node.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"node": {
|
|
13
|
+
"import": "./dist/entry.node.js",
|
|
14
|
+
"require": "./dist/entry.node.cjs"
|
|
15
|
+
},
|
|
16
|
+
"browser": "./dist/entry.web.js",
|
|
17
|
+
"default": "./dist/entry.web.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist/**",
|
|
22
|
+
"scripts/postinstall.js",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"dev": "tsup --watch",
|
|
28
|
+
"build": "tsup",
|
|
29
|
+
"postinstall": "node scripts/postinstall.js",
|
|
30
|
+
"vendor:harfbuzz": "node scripts/vendor-harfbuzz.js",
|
|
31
|
+
"example:node": "node examples/node-example.mjs",
|
|
32
|
+
"example:video": "node examples/node-video.mjs",
|
|
33
|
+
"example:web": "vite dev examples/web-example",
|
|
34
|
+
"test:caption-web": "vite dev examples/caption-tests",
|
|
35
|
+
"prepublishOnly": "node scripts/publish-guard.cjs && pnpm build",
|
|
36
|
+
"test": "node --test tests/build-verify.mjs"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public",
|
|
40
|
+
"registry": "https://registry.npmjs.org/"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18"
|
|
44
|
+
},
|
|
45
|
+
"sideEffects": false,
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@resvg/resvg-js": "^2.6.2",
|
|
48
|
+
"@resvg/resvg-wasm": "^2.6.2",
|
|
49
|
+
"@shotstack/schemas": "1.8.7",
|
|
50
|
+
"bidi-js": "^1.0.3",
|
|
51
|
+
"canvas": "npm:@napi-rs/canvas@^0.1.54",
|
|
52
|
+
"ffmpeg-static": "^5.2.0",
|
|
53
|
+
"fontkit": "^2.0.4",
|
|
54
|
+
"harfbuzzjs": "0.4.12",
|
|
55
|
+
"lru-cache": "^11.2.5",
|
|
56
|
+
"mp4-muxer": "^5.1.3",
|
|
57
|
+
"zod": "^4.2.0"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
61
|
+
"@semantic-release/commit-analyzer": "^13.0.1",
|
|
62
|
+
"@semantic-release/git": "^10.0.1",
|
|
63
|
+
"@semantic-release/github": "^12.0.6",
|
|
64
|
+
"@semantic-release/npm": "^13.1.5",
|
|
65
|
+
"@semantic-release/release-notes-generator": "^14.1.0",
|
|
66
|
+
"@types/node": "^20.14.10",
|
|
67
|
+
"semantic-release": "^25.0.3",
|
|
68
|
+
"tsup": "^8.2.3",
|
|
69
|
+
"typescript": "^5.5.3",
|
|
70
|
+
"vite": "^5.3.3",
|
|
71
|
+
"vite-plugin-top-level-await": "1.6.0",
|
|
72
|
+
"vite-plugin-wasm": "3.5.0"
|
|
73
|
+
},
|
|
74
|
+
"repository": {
|
|
75
|
+
"type": "git",
|
|
76
|
+
"url": "https://github.com/shotstack/shotstack-canvas.git"
|
|
77
|
+
}
|
|
78
|
+
}
|
package/scripts/postinstall.js
CHANGED
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Postinstall script to verify native canvas bindings are available
|
|
5
|
-
* This helps catch issues early and provides helpful guidance
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { platform as _platform, arch as _arch } from 'os';
|
|
9
|
-
import { dirname } from 'path';
|
|
10
|
-
import { readdirSync } from 'fs';
|
|
11
|
-
import { createRequire } from 'module';
|
|
12
|
-
|
|
13
|
-
const require = createRequire(import.meta.url);
|
|
14
|
-
|
|
15
|
-
const platform = _platform();
|
|
16
|
-
const arch = _arch();
|
|
17
|
-
|
|
18
|
-
// Map platform/arch to package names
|
|
19
|
-
const platformMap = {
|
|
20
|
-
'darwin-arm64': '@napi-rs/canvas-darwin-arm64',
|
|
21
|
-
'darwin-x64': '@napi-rs/canvas-darwin-x64',
|
|
22
|
-
'linux-arm64': '@napi-rs/canvas-linux-arm64-gnu',
|
|
23
|
-
'linux-x64': '@napi-rs/canvas-linux-x64-gnu',
|
|
24
|
-
'win32-x64': '@napi-rs/canvas-win32-x64-msvc',
|
|
25
|
-
'linux-arm': '@napi-rs/canvas-linux-arm-gnueabihf',
|
|
26
|
-
'android-arm64': '@napi-rs/canvas-android-arm64',
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
const platformKey = `${platform}-${arch}`;
|
|
30
|
-
const requiredPackage = platformMap[platformKey];
|
|
31
|
-
|
|
32
|
-
if (!requiredPackage) {
|
|
33
|
-
console.warn(`\n⚠️ Warning: Unsupported platform ${platformKey} for @napi-rs/canvas`);
|
|
34
|
-
console.warn(' Canvas rendering may not work on this platform.\n');
|
|
35
|
-
process.exit(0);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Check if the native binding package is installed
|
|
39
|
-
try {
|
|
40
|
-
const packagePath = require.resolve(`${requiredPackage}/package.json`);
|
|
41
|
-
const packageDir = dirname(packagePath);
|
|
42
|
-
|
|
43
|
-
// Verify the .node file exists
|
|
44
|
-
const nodeFiles = readdirSync(packageDir).filter(f => f.endsWith('.node'));
|
|
45
|
-
|
|
46
|
-
if (nodeFiles.length > 0) {
|
|
47
|
-
console.log(`✅ @shotstack/shotstack-canvas: Native canvas binding found for ${platformKey}`);
|
|
48
|
-
} else {
|
|
49
|
-
throw new Error('No .node file found');
|
|
50
|
-
}
|
|
51
|
-
} catch (error) {
|
|
52
|
-
console.warn(`\n⚠️ Warning: Native canvas binding not found for ${platformKey}`);
|
|
53
|
-
console.warn(` Expected package: ${requiredPackage}`);
|
|
54
|
-
console.warn('\n If you see "Cannot find native binding" errors, try:');
|
|
55
|
-
console.warn(' 1. Delete node_modules and package-lock.json');
|
|
56
|
-
console.warn(' 2. Run: npm install');
|
|
57
|
-
console.warn(` 3. Or manually install: npm install ${requiredPackage}\n`);
|
|
58
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Postinstall script to verify native canvas bindings are available
|
|
5
|
+
* This helps catch issues early and provides helpful guidance
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { platform as _platform, arch as _arch } from 'os';
|
|
9
|
+
import { dirname } from 'path';
|
|
10
|
+
import { readdirSync } from 'fs';
|
|
11
|
+
import { createRequire } from 'module';
|
|
12
|
+
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
|
|
15
|
+
const platform = _platform();
|
|
16
|
+
const arch = _arch();
|
|
17
|
+
|
|
18
|
+
// Map platform/arch to package names
|
|
19
|
+
const platformMap = {
|
|
20
|
+
'darwin-arm64': '@napi-rs/canvas-darwin-arm64',
|
|
21
|
+
'darwin-x64': '@napi-rs/canvas-darwin-x64',
|
|
22
|
+
'linux-arm64': '@napi-rs/canvas-linux-arm64-gnu',
|
|
23
|
+
'linux-x64': '@napi-rs/canvas-linux-x64-gnu',
|
|
24
|
+
'win32-x64': '@napi-rs/canvas-win32-x64-msvc',
|
|
25
|
+
'linux-arm': '@napi-rs/canvas-linux-arm-gnueabihf',
|
|
26
|
+
'android-arm64': '@napi-rs/canvas-android-arm64',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const platformKey = `${platform}-${arch}`;
|
|
30
|
+
const requiredPackage = platformMap[platformKey];
|
|
31
|
+
|
|
32
|
+
if (!requiredPackage) {
|
|
33
|
+
console.warn(`\n⚠️ Warning: Unsupported platform ${platformKey} for @napi-rs/canvas`);
|
|
34
|
+
console.warn(' Canvas rendering may not work on this platform.\n');
|
|
35
|
+
process.exit(0);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Check if the native binding package is installed
|
|
39
|
+
try {
|
|
40
|
+
const packagePath = require.resolve(`${requiredPackage}/package.json`);
|
|
41
|
+
const packageDir = dirname(packagePath);
|
|
42
|
+
|
|
43
|
+
// Verify the .node file exists
|
|
44
|
+
const nodeFiles = readdirSync(packageDir).filter(f => f.endsWith('.node'));
|
|
45
|
+
|
|
46
|
+
if (nodeFiles.length > 0) {
|
|
47
|
+
console.log(`✅ @shotstack/shotstack-canvas: Native canvas binding found for ${platformKey}`);
|
|
48
|
+
} else {
|
|
49
|
+
throw new Error('No .node file found');
|
|
50
|
+
}
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.warn(`\n⚠️ Warning: Native canvas binding not found for ${platformKey}`);
|
|
53
|
+
console.warn(` Expected package: ${requiredPackage}`);
|
|
54
|
+
console.warn('\n If you see "Cannot find native binding" errors, try:');
|
|
55
|
+
console.warn(' 1. Delete node_modules and package-lock.json');
|
|
56
|
+
console.warn(' 2. Run: npm install');
|
|
57
|
+
console.warn(` 3. Or manually install: npm install ${requiredPackage}\n`);
|
|
58
|
+
}
|