@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.
@@ -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(this.wasmBaseURL);
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
- ctx.save();
1210
- ctx.fillRect(0, 0, canvas.width, canvas.height);
1211
- ctx.restore();
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
- if (color) {
1215
- const c = parseHex6(color, opacity);
1216
- ctx.fillStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
1217
- if (radius && radius > 0) {
1218
- ctx.save();
1219
- ctx.beginPath();
1220
- roundRectPath(ctx, 0, 0, op.width, op.height, radius);
1221
- ctx.fill();
1222
- ctx.restore();
1223
- } else {
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
- ctx.save();
1233
- ctx.translate(fillOp.x, fillOp.y);
1234
- const s = fillOp.scale ?? 1;
1235
- ctx.scale(s, -s);
1236
- ctx.beginPath();
1237
- drawSvgPathOnCtx(ctx, fillOp.path);
1238
- const bbox = fillOp.gradientBBox ?? globalBox;
1239
- const fill = makeGradientFromBBox(ctx, fillOp.fill, bbox);
1240
- ctx.fillStyle = fill;
1241
- ctx.fill();
1242
- ctx.restore();
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
- ctx.save();
1248
- ctx.translate(strokeOp.x, strokeOp.y);
1249
- const s = strokeOp.scale ?? 1;
1250
- ctx.scale(s, -s);
1251
- const invAbs = 1 / Math.abs(s);
1252
- ctx.beginPath();
1253
- drawSvgPathOnCtx(ctx, strokeOp.path);
1254
- const c = parseHex6(strokeOp.color, strokeOp.opacity);
1255
- ctx.strokeStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
1256
- ctx.lineWidth = strokeOp.width * invAbs;
1257
- ctx.lineJoin = "round";
1258
- ctx.lineCap = "round";
1259
- ctx.stroke();
1260
- ctx.restore();
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
- ctx.save();
1265
- const c = parseHex6(op.color, op.opacity);
1266
- ctx.strokeStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
1267
- ctx.lineWidth = op.width;
1268
- ctx.beginPath();
1269
- ctx.moveTo(op.from.x, op.from.y);
1270
- ctx.lineTo(op.to.x, op.to.y);
1271
- ctx.stroke();
1272
- ctx.restore();
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");