@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.cjs
CHANGED
|
@@ -164,9 +164,62 @@ var RichTextAssetSchema = import_joi.default.object({
|
|
|
164
164
|
|
|
165
165
|
// src/wasm/hb-loader.ts
|
|
166
166
|
var hbSingleton = null;
|
|
167
|
+
function isNode() {
|
|
168
|
+
return typeof process !== "undefined" && process.versions != null && process.versions.node != null;
|
|
169
|
+
}
|
|
170
|
+
async function loadWasmWeb(wasmBaseURL) {
|
|
171
|
+
try {
|
|
172
|
+
if (wasmBaseURL) {
|
|
173
|
+
const url = wasmBaseURL.endsWith(".wasm") ? wasmBaseURL : wasmBaseURL.endsWith("/") ? `${wasmBaseURL}hb.wasm` : `${wasmBaseURL}/hb.wasm`;
|
|
174
|
+
console.log(`Fetching WASM from: ${url}`);
|
|
175
|
+
const response = await fetch(url);
|
|
176
|
+
if (response.ok) {
|
|
177
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
178
|
+
const bytes = new Uint8Array(arrayBuffer);
|
|
179
|
+
if (bytes.length >= 4 && bytes[0] === 0 && bytes[1] === 97 && bytes[2] === 115 && bytes[3] === 109) {
|
|
180
|
+
console.log(`\u2705 Valid WASM binary loaded (${bytes.length} bytes)`);
|
|
181
|
+
return arrayBuffer;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return void 0;
|
|
186
|
+
} catch (err) {
|
|
187
|
+
console.error("Error in loadWasmWeb:", err);
|
|
188
|
+
return void 0;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function setupWasmFetchInterceptor(wasmBinary) {
|
|
192
|
+
const originalFetch = window.fetch;
|
|
193
|
+
window.fetch = function(input, init) {
|
|
194
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
195
|
+
if (url.includes("hb.wasm") || url.endsWith(".wasm")) {
|
|
196
|
+
console.log(`\u{1F504} Intercepted fetch for: ${url}`);
|
|
197
|
+
return Promise.resolve(
|
|
198
|
+
new Response(wasmBinary, {
|
|
199
|
+
status: 200,
|
|
200
|
+
statusText: "OK",
|
|
201
|
+
headers: {
|
|
202
|
+
"Content-Type": "application/wasm",
|
|
203
|
+
"Content-Length": wasmBinary.byteLength.toString()
|
|
204
|
+
}
|
|
205
|
+
})
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
return originalFetch.apply(this, [input, init]);
|
|
209
|
+
};
|
|
210
|
+
}
|
|
167
211
|
async function initHB(wasmBaseURL) {
|
|
168
212
|
if (hbSingleton) return hbSingleton;
|
|
169
213
|
try {
|
|
214
|
+
let wasmBinary;
|
|
215
|
+
wasmBinary = await loadWasmWeb(wasmBaseURL);
|
|
216
|
+
if (!wasmBinary) {
|
|
217
|
+
throw new Error("Failed to load WASM binary from any source");
|
|
218
|
+
}
|
|
219
|
+
console.log(`\u2705 WASM binary loaded successfully (${wasmBinary.byteLength} bytes)`);
|
|
220
|
+
if (!isNode()) {
|
|
221
|
+
setupWasmFetchInterceptor(wasmBinary);
|
|
222
|
+
}
|
|
170
223
|
const mod = await import("harfbuzzjs");
|
|
171
224
|
const candidate = mod.default;
|
|
172
225
|
let hb;
|
|
@@ -180,10 +233,11 @@ async function initHB(wasmBaseURL) {
|
|
|
180
233
|
if (!hb || typeof hb.createBuffer !== "function" || typeof hb.createFont !== "function") {
|
|
181
234
|
throw new Error("Failed to initialize HarfBuzz: unexpected export shape from 'harfbuzzjs'.");
|
|
182
235
|
}
|
|
183
|
-
void wasmBaseURL;
|
|
184
236
|
hbSingleton = hb;
|
|
237
|
+
console.log("\u2705 HarfBuzz initialized successfully");
|
|
185
238
|
return hbSingleton;
|
|
186
239
|
} catch (err) {
|
|
240
|
+
console.error("Failed to initialize HarfBuzz:", err);
|
|
187
241
|
throw new Error(
|
|
188
242
|
`Failed to initialize HarfBuzz: ${err instanceof Error ? err.message : String(err)}`
|
|
189
243
|
);
|
|
@@ -222,7 +276,7 @@ var FontRegistry = class _FontRegistry {
|
|
|
222
276
|
}
|
|
223
277
|
async _doInit() {
|
|
224
278
|
try {
|
|
225
|
-
this.hb = await initHB(
|
|
279
|
+
this.hb = await initHB("https://shotstack-ingest-api-dev-sources.s3.ap-southeast-2.amazonaws.com/euo5r93oyr/zzz01k9h-yycyx-2x2y6-qx9bj-7n567b/source.wasm");
|
|
226
280
|
} catch (error) {
|
|
227
281
|
this.initPromise = void 0;
|
|
228
282
|
throw error;
|
|
@@ -1192,87 +1246,141 @@ async function createNodePainter(opts) {
|
|
|
1192
1246
|
);
|
|
1193
1247
|
const ctx = canvas.getContext("2d");
|
|
1194
1248
|
if (!ctx) throw new Error("2D context unavailable in Node (canvas).");
|
|
1249
|
+
const offscreenCanvas = createCanvas(canvas.width, canvas.height);
|
|
1250
|
+
const offscreenCtx = offscreenCanvas.getContext("2d");
|
|
1195
1251
|
const api = {
|
|
1196
1252
|
async render(ops) {
|
|
1197
1253
|
const globalBox = computeGlobalTextBounds(ops);
|
|
1254
|
+
let needsAlphaExtraction = false;
|
|
1198
1255
|
for (const op of ops) {
|
|
1199
1256
|
if (op.op === "BeginFrame") {
|
|
1200
|
-
if (op.clear) ctx.clearRect(0, 0, op.width, op.height);
|
|
1201
1257
|
const dpr = op.pixelRatio ?? opts.pixelRatio;
|
|
1202
1258
|
const wantW = Math.floor(op.width * dpr);
|
|
1203
1259
|
const wantH = Math.floor(op.height * dpr);
|
|
1204
1260
|
if (canvas.width !== wantW || canvas.height !== wantH) {
|
|
1205
1261
|
canvas.width = wantW;
|
|
1206
1262
|
canvas.height = wantH;
|
|
1263
|
+
offscreenCanvas.width = wantW;
|
|
1264
|
+
offscreenCanvas.height = wantH;
|
|
1207
1265
|
}
|
|
1208
1266
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1267
|
+
offscreenCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
1268
|
+
const hasBackground = !!(op.bg && op.bg.color);
|
|
1269
|
+
needsAlphaExtraction = !hasBackground;
|
|
1212
1270
|
if (op.bg && op.bg.color) {
|
|
1213
1271
|
const { color, opacity, radius } = op.bg;
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
ctx.fillRect(0, 0, op.width, op.height);
|
|
1225
|
-
}
|
|
1272
|
+
const c = parseHex6(color, opacity);
|
|
1273
|
+
ctx.fillStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
|
|
1274
|
+
if (radius && radius > 0) {
|
|
1275
|
+
ctx.save();
|
|
1276
|
+
ctx.beginPath();
|
|
1277
|
+
roundRectPath(ctx, 0, 0, op.width, op.height, radius);
|
|
1278
|
+
ctx.fill();
|
|
1279
|
+
ctx.restore();
|
|
1280
|
+
} else {
|
|
1281
|
+
ctx.fillRect(0, 0, op.width, op.height);
|
|
1226
1282
|
}
|
|
1283
|
+
} else {
|
|
1284
|
+
ctx.fillStyle = "rgb(255, 255, 255)";
|
|
1285
|
+
ctx.fillRect(0, 0, op.width, op.height);
|
|
1286
|
+
offscreenCtx.fillStyle = "rgb(0, 0, 0)";
|
|
1287
|
+
offscreenCtx.fillRect(0, 0, op.width, op.height);
|
|
1227
1288
|
}
|
|
1228
1289
|
continue;
|
|
1229
1290
|
}
|
|
1291
|
+
const renderToBoth = (renderFn) => {
|
|
1292
|
+
if (needsAlphaExtraction) {
|
|
1293
|
+
renderFn(ctx);
|
|
1294
|
+
renderFn(offscreenCtx);
|
|
1295
|
+
} else {
|
|
1296
|
+
renderFn(ctx);
|
|
1297
|
+
}
|
|
1298
|
+
};
|
|
1230
1299
|
if (op.op === "FillPath") {
|
|
1231
1300
|
const fillOp = op;
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1301
|
+
renderToBoth((context) => {
|
|
1302
|
+
context.save();
|
|
1303
|
+
context.translate(fillOp.x, fillOp.y);
|
|
1304
|
+
const s = fillOp.scale ?? 1;
|
|
1305
|
+
context.scale(s, -s);
|
|
1306
|
+
context.beginPath();
|
|
1307
|
+
drawSvgPathOnCtx(context, fillOp.path);
|
|
1308
|
+
const bbox = fillOp.gradientBBox ?? globalBox;
|
|
1309
|
+
const fill = makeGradientFromBBox(context, fillOp.fill, bbox);
|
|
1310
|
+
context.fillStyle = fill;
|
|
1311
|
+
context.fill();
|
|
1312
|
+
context.restore();
|
|
1313
|
+
});
|
|
1243
1314
|
continue;
|
|
1244
1315
|
}
|
|
1245
1316
|
if (op.op === "StrokePath") {
|
|
1246
1317
|
const strokeOp = op;
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1318
|
+
renderToBoth((context) => {
|
|
1319
|
+
context.save();
|
|
1320
|
+
context.translate(strokeOp.x, strokeOp.y);
|
|
1321
|
+
const s = strokeOp.scale ?? 1;
|
|
1322
|
+
context.scale(s, -s);
|
|
1323
|
+
const invAbs = 1 / Math.abs(s);
|
|
1324
|
+
context.beginPath();
|
|
1325
|
+
drawSvgPathOnCtx(context, strokeOp.path);
|
|
1326
|
+
const c = parseHex6(strokeOp.color, strokeOp.opacity);
|
|
1327
|
+
context.strokeStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
|
|
1328
|
+
context.lineWidth = strokeOp.width * invAbs;
|
|
1329
|
+
context.lineJoin = "round";
|
|
1330
|
+
context.lineCap = "round";
|
|
1331
|
+
context.stroke();
|
|
1332
|
+
context.restore();
|
|
1333
|
+
});
|
|
1261
1334
|
continue;
|
|
1262
1335
|
}
|
|
1263
1336
|
if (op.op === "DecorationLine") {
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1337
|
+
renderToBoth((context) => {
|
|
1338
|
+
context.save();
|
|
1339
|
+
const c = parseHex6(op.color, op.opacity);
|
|
1340
|
+
context.strokeStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
|
|
1341
|
+
context.lineWidth = op.width;
|
|
1342
|
+
context.beginPath();
|
|
1343
|
+
context.moveTo(op.from.x, op.from.y);
|
|
1344
|
+
context.lineTo(op.to.x, op.to.y);
|
|
1345
|
+
context.stroke();
|
|
1346
|
+
context.restore();
|
|
1347
|
+
});
|
|
1273
1348
|
continue;
|
|
1274
1349
|
}
|
|
1275
1350
|
}
|
|
1351
|
+
if (needsAlphaExtraction) {
|
|
1352
|
+
const whiteData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
1353
|
+
const blackData = offscreenCtx.getImageData(0, 0, canvas.width, canvas.height);
|
|
1354
|
+
const white = whiteData.data;
|
|
1355
|
+
const black = blackData.data;
|
|
1356
|
+
for (let i = 0; i < white.length; i += 4) {
|
|
1357
|
+
const wr = white[i];
|
|
1358
|
+
const wg = white[i + 1];
|
|
1359
|
+
const wb = white[i + 2];
|
|
1360
|
+
const br = black[i];
|
|
1361
|
+
const bg = black[i + 1];
|
|
1362
|
+
const bb = black[i + 2];
|
|
1363
|
+
const alpha = 255 - Math.max(wr - br, wg - bg, wb - bb);
|
|
1364
|
+
if (alpha <= 2) {
|
|
1365
|
+
white[i] = 0;
|
|
1366
|
+
white[i + 1] = 0;
|
|
1367
|
+
white[i + 2] = 0;
|
|
1368
|
+
white[i + 3] = 0;
|
|
1369
|
+
} else if (alpha >= 253) {
|
|
1370
|
+
white[i] = br;
|
|
1371
|
+
white[i + 1] = bg;
|
|
1372
|
+
white[i + 2] = bb;
|
|
1373
|
+
white[i + 3] = 255;
|
|
1374
|
+
} else {
|
|
1375
|
+
const alphaFactor = 255 / alpha;
|
|
1376
|
+
white[i] = Math.min(255, Math.max(0, Math.round(br * alphaFactor)));
|
|
1377
|
+
white[i + 1] = Math.min(255, Math.max(0, Math.round(bg * alphaFactor)));
|
|
1378
|
+
white[i + 2] = Math.min(255, Math.max(0, Math.round(bb * alphaFactor)));
|
|
1379
|
+
white[i + 3] = alpha;
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
ctx.putImageData(whiteData, 0, 0);
|
|
1383
|
+
}
|
|
1276
1384
|
},
|
|
1277
1385
|
async toPNG() {
|
|
1278
1386
|
return canvas.toBuffer("image/png");
|