@shotstack/shotstack-canvas 1.1.5 → 1.1.7
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 +160 -52
- package/dist/entry.node.cjs.map +1 -1
- package/dist/entry.node.js +160 -52
- package/dist/entry.node.js.map +1 -1
- package/dist/entry.web.js +57 -3
- package/dist/entry.web.js.map +1 -1
- package/package.json +2 -3
- package/public/hb.wasm +0 -0
package/dist/entry.node.js
CHANGED
|
@@ -126,9 +126,62 @@ var RichTextAssetSchema = Joi.object({
|
|
|
126
126
|
|
|
127
127
|
// src/wasm/hb-loader.ts
|
|
128
128
|
var hbSingleton = null;
|
|
129
|
+
function isNode() {
|
|
130
|
+
return typeof process !== "undefined" && process.versions != null && process.versions.node != null;
|
|
131
|
+
}
|
|
132
|
+
async function loadWasmWeb(wasmBaseURL) {
|
|
133
|
+
try {
|
|
134
|
+
if (wasmBaseURL) {
|
|
135
|
+
const url = wasmBaseURL.endsWith(".wasm") ? wasmBaseURL : wasmBaseURL.endsWith("/") ? `${wasmBaseURL}hb.wasm` : `${wasmBaseURL}/hb.wasm`;
|
|
136
|
+
console.log(`Fetching WASM from: ${url}`);
|
|
137
|
+
const response = await fetch(url);
|
|
138
|
+
if (response.ok) {
|
|
139
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
140
|
+
const bytes = new Uint8Array(arrayBuffer);
|
|
141
|
+
if (bytes.length >= 4 && bytes[0] === 0 && bytes[1] === 97 && bytes[2] === 115 && bytes[3] === 109) {
|
|
142
|
+
console.log(`\u2705 Valid WASM binary loaded (${bytes.length} bytes)`);
|
|
143
|
+
return arrayBuffer;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return void 0;
|
|
148
|
+
} catch (err) {
|
|
149
|
+
console.error("Error in loadWasmWeb:", err);
|
|
150
|
+
return void 0;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function setupWasmFetchInterceptor(wasmBinary) {
|
|
154
|
+
const originalFetch = window.fetch;
|
|
155
|
+
window.fetch = function(input, init) {
|
|
156
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
157
|
+
if (url.includes("hb.wasm") || url.endsWith(".wasm")) {
|
|
158
|
+
console.log(`\u{1F504} Intercepted fetch for: ${url}`);
|
|
159
|
+
return Promise.resolve(
|
|
160
|
+
new Response(wasmBinary, {
|
|
161
|
+
status: 200,
|
|
162
|
+
statusText: "OK",
|
|
163
|
+
headers: {
|
|
164
|
+
"Content-Type": "application/wasm",
|
|
165
|
+
"Content-Length": wasmBinary.byteLength.toString()
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
return originalFetch.apply(this, [input, init]);
|
|
171
|
+
};
|
|
172
|
+
}
|
|
129
173
|
async function initHB(wasmBaseURL) {
|
|
130
174
|
if (hbSingleton) return hbSingleton;
|
|
131
175
|
try {
|
|
176
|
+
let wasmBinary;
|
|
177
|
+
wasmBinary = await loadWasmWeb(wasmBaseURL);
|
|
178
|
+
if (!wasmBinary) {
|
|
179
|
+
throw new Error("Failed to load WASM binary from any source");
|
|
180
|
+
}
|
|
181
|
+
console.log(`\u2705 WASM binary loaded successfully (${wasmBinary.byteLength} bytes)`);
|
|
182
|
+
if (!isNode()) {
|
|
183
|
+
setupWasmFetchInterceptor(wasmBinary);
|
|
184
|
+
}
|
|
132
185
|
const mod = await import("harfbuzzjs");
|
|
133
186
|
const candidate = mod.default;
|
|
134
187
|
let hb;
|
|
@@ -142,10 +195,11 @@ async function initHB(wasmBaseURL) {
|
|
|
142
195
|
if (!hb || typeof hb.createBuffer !== "function" || typeof hb.createFont !== "function") {
|
|
143
196
|
throw new Error("Failed to initialize HarfBuzz: unexpected export shape from 'harfbuzzjs'.");
|
|
144
197
|
}
|
|
145
|
-
void wasmBaseURL;
|
|
146
198
|
hbSingleton = hb;
|
|
199
|
+
console.log("\u2705 HarfBuzz initialized successfully");
|
|
147
200
|
return hbSingleton;
|
|
148
201
|
} catch (err) {
|
|
202
|
+
console.error("Failed to initialize HarfBuzz:", err);
|
|
149
203
|
throw new Error(
|
|
150
204
|
`Failed to initialize HarfBuzz: ${err instanceof Error ? err.message : String(err)}`
|
|
151
205
|
);
|
|
@@ -184,7 +238,7 @@ var FontRegistry = class _FontRegistry {
|
|
|
184
238
|
}
|
|
185
239
|
async _doInit() {
|
|
186
240
|
try {
|
|
187
|
-
this.hb = await initHB(
|
|
241
|
+
this.hb = await initHB("https://shotstack-ingest-api-dev-sources.s3.ap-southeast-2.amazonaws.com/euo5r93oyr/zzz01k9h-yycyx-2x2y6-qx9bj-7n567b/source.wasm");
|
|
188
242
|
} catch (error) {
|
|
189
243
|
this.initPromise = void 0;
|
|
190
244
|
throw error;
|
|
@@ -1154,87 +1208,141 @@ async function createNodePainter(opts) {
|
|
|
1154
1208
|
);
|
|
1155
1209
|
const ctx = canvas.getContext("2d");
|
|
1156
1210
|
if (!ctx) throw new Error("2D context unavailable in Node (canvas).");
|
|
1211
|
+
const offscreenCanvas = createCanvas(canvas.width, canvas.height);
|
|
1212
|
+
const offscreenCtx = offscreenCanvas.getContext("2d");
|
|
1157
1213
|
const api = {
|
|
1158
1214
|
async render(ops) {
|
|
1159
1215
|
const globalBox = computeGlobalTextBounds(ops);
|
|
1216
|
+
let needsAlphaExtraction = false;
|
|
1160
1217
|
for (const op of ops) {
|
|
1161
1218
|
if (op.op === "BeginFrame") {
|
|
1162
|
-
if (op.clear) ctx.clearRect(0, 0, op.width, op.height);
|
|
1163
1219
|
const dpr = op.pixelRatio ?? opts.pixelRatio;
|
|
1164
1220
|
const wantW = Math.floor(op.width * dpr);
|
|
1165
1221
|
const wantH = Math.floor(op.height * dpr);
|
|
1166
1222
|
if (canvas.width !== wantW || canvas.height !== wantH) {
|
|
1167
1223
|
canvas.width = wantW;
|
|
1168
1224
|
canvas.height = wantH;
|
|
1225
|
+
offscreenCanvas.width = wantW;
|
|
1226
|
+
offscreenCanvas.height = wantH;
|
|
1169
1227
|
}
|
|
1170
1228
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1229
|
+
offscreenCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
1230
|
+
const hasBackground = !!(op.bg && op.bg.color);
|
|
1231
|
+
needsAlphaExtraction = !hasBackground;
|
|
1174
1232
|
if (op.bg && op.bg.color) {
|
|
1175
1233
|
const { color, opacity, radius } = op.bg;
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
ctx.fillRect(0, 0, op.width, op.height);
|
|
1187
|
-
}
|
|
1234
|
+
const c = parseHex6(color, opacity);
|
|
1235
|
+
ctx.fillStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
|
|
1236
|
+
if (radius && radius > 0) {
|
|
1237
|
+
ctx.save();
|
|
1238
|
+
ctx.beginPath();
|
|
1239
|
+
roundRectPath(ctx, 0, 0, op.width, op.height, radius);
|
|
1240
|
+
ctx.fill();
|
|
1241
|
+
ctx.restore();
|
|
1242
|
+
} else {
|
|
1243
|
+
ctx.fillRect(0, 0, op.width, op.height);
|
|
1188
1244
|
}
|
|
1245
|
+
} else {
|
|
1246
|
+
ctx.fillStyle = "rgb(255, 255, 255)";
|
|
1247
|
+
ctx.fillRect(0, 0, op.width, op.height);
|
|
1248
|
+
offscreenCtx.fillStyle = "rgb(0, 0, 0)";
|
|
1249
|
+
offscreenCtx.fillRect(0, 0, op.width, op.height);
|
|
1189
1250
|
}
|
|
1190
1251
|
continue;
|
|
1191
1252
|
}
|
|
1253
|
+
const renderToBoth = (renderFn) => {
|
|
1254
|
+
if (needsAlphaExtraction) {
|
|
1255
|
+
renderFn(ctx);
|
|
1256
|
+
renderFn(offscreenCtx);
|
|
1257
|
+
} else {
|
|
1258
|
+
renderFn(ctx);
|
|
1259
|
+
}
|
|
1260
|
+
};
|
|
1192
1261
|
if (op.op === "FillPath") {
|
|
1193
1262
|
const fillOp = op;
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1263
|
+
renderToBoth((context) => {
|
|
1264
|
+
context.save();
|
|
1265
|
+
context.translate(fillOp.x, fillOp.y);
|
|
1266
|
+
const s = fillOp.scale ?? 1;
|
|
1267
|
+
context.scale(s, -s);
|
|
1268
|
+
context.beginPath();
|
|
1269
|
+
drawSvgPathOnCtx(context, fillOp.path);
|
|
1270
|
+
const bbox = fillOp.gradientBBox ?? globalBox;
|
|
1271
|
+
const fill = makeGradientFromBBox(context, fillOp.fill, bbox);
|
|
1272
|
+
context.fillStyle = fill;
|
|
1273
|
+
context.fill();
|
|
1274
|
+
context.restore();
|
|
1275
|
+
});
|
|
1205
1276
|
continue;
|
|
1206
1277
|
}
|
|
1207
1278
|
if (op.op === "StrokePath") {
|
|
1208
1279
|
const strokeOp = op;
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1280
|
+
renderToBoth((context) => {
|
|
1281
|
+
context.save();
|
|
1282
|
+
context.translate(strokeOp.x, strokeOp.y);
|
|
1283
|
+
const s = strokeOp.scale ?? 1;
|
|
1284
|
+
context.scale(s, -s);
|
|
1285
|
+
const invAbs = 1 / Math.abs(s);
|
|
1286
|
+
context.beginPath();
|
|
1287
|
+
drawSvgPathOnCtx(context, strokeOp.path);
|
|
1288
|
+
const c = parseHex6(strokeOp.color, strokeOp.opacity);
|
|
1289
|
+
context.strokeStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
|
|
1290
|
+
context.lineWidth = strokeOp.width * invAbs;
|
|
1291
|
+
context.lineJoin = "round";
|
|
1292
|
+
context.lineCap = "round";
|
|
1293
|
+
context.stroke();
|
|
1294
|
+
context.restore();
|
|
1295
|
+
});
|
|
1223
1296
|
continue;
|
|
1224
1297
|
}
|
|
1225
1298
|
if (op.op === "DecorationLine") {
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1299
|
+
renderToBoth((context) => {
|
|
1300
|
+
context.save();
|
|
1301
|
+
const c = parseHex6(op.color, op.opacity);
|
|
1302
|
+
context.strokeStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
|
|
1303
|
+
context.lineWidth = op.width;
|
|
1304
|
+
context.beginPath();
|
|
1305
|
+
context.moveTo(op.from.x, op.from.y);
|
|
1306
|
+
context.lineTo(op.to.x, op.to.y);
|
|
1307
|
+
context.stroke();
|
|
1308
|
+
context.restore();
|
|
1309
|
+
});
|
|
1235
1310
|
continue;
|
|
1236
1311
|
}
|
|
1237
1312
|
}
|
|
1313
|
+
if (needsAlphaExtraction) {
|
|
1314
|
+
const whiteData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
1315
|
+
const blackData = offscreenCtx.getImageData(0, 0, canvas.width, canvas.height);
|
|
1316
|
+
const white = whiteData.data;
|
|
1317
|
+
const black = blackData.data;
|
|
1318
|
+
for (let i = 0; i < white.length; i += 4) {
|
|
1319
|
+
const wr = white[i];
|
|
1320
|
+
const wg = white[i + 1];
|
|
1321
|
+
const wb = white[i + 2];
|
|
1322
|
+
const br = black[i];
|
|
1323
|
+
const bg = black[i + 1];
|
|
1324
|
+
const bb = black[i + 2];
|
|
1325
|
+
const alpha = 255 - Math.max(wr - br, wg - bg, wb - bb);
|
|
1326
|
+
if (alpha <= 2) {
|
|
1327
|
+
white[i] = 0;
|
|
1328
|
+
white[i + 1] = 0;
|
|
1329
|
+
white[i + 2] = 0;
|
|
1330
|
+
white[i + 3] = 0;
|
|
1331
|
+
} else if (alpha >= 253) {
|
|
1332
|
+
white[i] = br;
|
|
1333
|
+
white[i + 1] = bg;
|
|
1334
|
+
white[i + 2] = bb;
|
|
1335
|
+
white[i + 3] = 255;
|
|
1336
|
+
} else {
|
|
1337
|
+
const alphaFactor = 255 / alpha;
|
|
1338
|
+
white[i] = Math.min(255, Math.max(0, Math.round(br * alphaFactor)));
|
|
1339
|
+
white[i + 1] = Math.min(255, Math.max(0, Math.round(bg * alphaFactor)));
|
|
1340
|
+
white[i + 2] = Math.min(255, Math.max(0, Math.round(bb * alphaFactor)));
|
|
1341
|
+
white[i + 3] = alpha;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
ctx.putImageData(whiteData, 0, 0);
|
|
1345
|
+
}
|
|
1238
1346
|
},
|
|
1239
1347
|
async toPNG() {
|
|
1240
1348
|
return canvas.toBuffer("image/png");
|