@shotstack/shotstack-canvas 1.6.1 → 1.6.3

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.
@@ -196,13 +196,11 @@ function bufferToArrayBuffer(buffer) {
196
196
  var DEFAULT_WASM_URL = "https://shotstack-ingest-api-dev-sources.s3.ap-southeast-2.amazonaws.com/euo5r93oyr/zzz01k9h-yycyx-2x2y6-qx9bj-7n567b/source.wasm";
197
197
  async function fetchWasmFromUrl(url) {
198
198
  try {
199
- console.log(`\u{1F310} Fetching WASM from URL: ${url}`);
200
199
  const response = await fetch(url);
201
200
  if (response.ok) {
202
201
  const arrayBuffer = await response.arrayBuffer();
203
202
  const bytes = new Uint8Array(arrayBuffer);
204
203
  if (bytes.length >= 4 && bytes[0] === 0 && bytes[1] === 97 && bytes[2] === 115 && bytes[3] === 109) {
205
- console.log(`\u2705 Fetched WASM from URL (${bytes.length} bytes)`);
206
204
  return arrayBuffer;
207
205
  } else {
208
206
  console.error(`\u274C Invalid WASM magic number from URL: ${url}`);
@@ -250,21 +248,17 @@ async function loadWasmNode() {
250
248
  "/var/task/node_modules/harfbuzzjs/hb.wasm",
251
249
  "/var/task/node_modules/@shotstack/shotstack-canvas/assets/wasm/hb.wasm"
252
250
  );
253
- console.log(`\u{1F50D} Searching for WASM in ${candidates.length} local paths...`);
254
251
  for (const candidate of candidates) {
255
252
  try {
256
253
  const buffer = await readFile2(candidate);
257
- console.log(`\u2705 Found WASM at: ${candidate}`);
258
254
  return bufferToArrayBuffer(buffer);
259
255
  } catch {
260
256
  continue;
261
257
  }
262
258
  }
263
- console.log("\u{1F4C2} Local WASM not found, fetching from URL...");
264
259
  return await fetchWasmFromUrl(DEFAULT_WASM_URL);
265
260
  } catch (err) {
266
261
  console.error("Error in loadWasmNode:", err);
267
- console.log("\u26A0\uFE0F Error during local search, falling back to URL...");
268
262
  return await fetchWasmFromUrl(DEFAULT_WASM_URL);
269
263
  }
270
264
  }
@@ -272,13 +266,11 @@ async function loadWasmWeb(wasmBaseURL) {
272
266
  try {
273
267
  if (wasmBaseURL) {
274
268
  const url = wasmBaseURL.endsWith(".wasm") ? wasmBaseURL : wasmBaseURL.endsWith("/") ? `${wasmBaseURL}hb.wasm` : `${wasmBaseURL}/hb.wasm`;
275
- console.log(`Fetching WASM from: ${url}`);
276
269
  const response = await fetch(url);
277
270
  if (response.ok) {
278
271
  const arrayBuffer = await response.arrayBuffer();
279
272
  const bytes = new Uint8Array(arrayBuffer);
280
273
  if (bytes.length >= 4 && bytes[0] === 0 && bytes[1] === 97 && bytes[2] === 115 && bytes[3] === 109) {
281
- console.log(`\u2705 Valid WASM binary loaded (${bytes.length} bytes)`);
282
274
  return arrayBuffer;
283
275
  }
284
276
  }
@@ -294,7 +286,6 @@ function setupWasmInterceptors(wasmBinary) {
294
286
  window.fetch = function(input, init) {
295
287
  const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
296
288
  if (url.includes("hb.wasm") || url.endsWith(".wasm") && !url.includes("source.wasm")) {
297
- console.log(`\u{1F504} Intercepted fetch for: ${url}`);
298
289
  return Promise.resolve(
299
290
  new Response(wasmBinary.slice(0), {
300
291
  status: 200,
@@ -310,13 +301,9 @@ function setupWasmInterceptors(wasmBinary) {
310
301
  };
311
302
  const originalInstantiate = WebAssembly.instantiate;
312
303
  WebAssembly.instantiate = async function(bufferSourceOrModule, importObject) {
313
- console.log(
314
- `\u{1F504} WebAssembly.instantiate called, type: ${bufferSourceOrModule instanceof WebAssembly.Module ? "Module" : "BufferSource"}`
315
- );
316
304
  if (bufferSourceOrModule instanceof WebAssembly.Module) {
317
305
  return originalInstantiate.call(WebAssembly, bufferSourceOrModule, importObject);
318
306
  }
319
- console.log(`\u{1F504} Intercepted WebAssembly.instantiate, using pre-loaded WASM binary`);
320
307
  const module2 = await WebAssembly.compile(wasmBinary);
321
308
  const instance = await originalInstantiate.call(
322
309
  WebAssembly,
@@ -332,14 +319,12 @@ function setupWasmInterceptors(wasmBinary) {
332
319
  const response = await source;
333
320
  const url = response.url || "";
334
321
  if (url.includes("hb.wasm") || url.endsWith(".wasm") && !url.includes("source.wasm")) {
335
- console.log(`\u{1F504} Intercepted instantiateStreaming for: ${url}`);
336
322
  const module2 = await WebAssembly.compile(wasmBinary);
337
323
  const instance = await WebAssembly.instantiate(module2, importObject);
338
324
  return { module: module2, instance };
339
325
  }
340
326
  return originalInstantiateStreaming.call(WebAssembly, response, importObject);
341
327
  } catch (err) {
342
- console.log("\u{1F504} instantiateStreaming failed, using pre-loaded binary");
343
328
  const module2 = await WebAssembly.compile(wasmBinary);
344
329
  const instance = await WebAssembly.instantiate(module2, importObject);
345
330
  return { module: module2, instance };
@@ -359,33 +344,25 @@ async function initHB(wasmBaseURL) {
359
344
  if (!wasmBinary) {
360
345
  throw new Error("Failed to load WASM binary from any source");
361
346
  }
362
- console.log(`\u2705 WASM binary loaded successfully (${wasmBinary.byteLength} bytes)`);
363
347
  if (!isNode()) {
364
348
  setupWasmInterceptors(wasmBinary);
365
349
  window.Module = {
366
350
  wasmBinary,
367
351
  locateFile: (path) => {
368
- console.log(`\u{1F50D} locateFile called for: ${path}`);
369
352
  return path;
370
353
  }
371
354
  };
372
- console.log(`\u{1F30D} Set global Module.wasmBinary (${wasmBinary.byteLength} bytes)`);
373
355
  }
374
- console.log("\u{1F504} Importing harfbuzzjs/hb.js (factory)");
375
356
  const hbModule = await import("harfbuzzjs/hb.js");
376
357
  const hbFactory = hbModule.default || hbModule;
377
- console.log("\u{1F504} Calling hb factory with wasmBinary");
378
358
  const hbInstance = await hbFactory({ wasmBinary });
379
- console.log("\u{1F504} Importing harfbuzzjs/hbjs.js (wrapper)");
380
359
  const hbjsModule = await import("harfbuzzjs/hbjs.js");
381
360
  const hbjsWrapper = hbjsModule.default || hbjsModule;
382
- console.log("\u{1F504} Wrapping hb instance");
383
361
  const hb = hbjsWrapper(hbInstance);
384
362
  if (!hb || typeof hb.createBuffer !== "function" || typeof hb.createFont !== "function") {
385
363
  throw new Error("Failed to initialize HarfBuzz: unexpected export shape from 'harfbuzzjs'.");
386
364
  }
387
365
  hbSingleton = hb;
388
- console.log("\u2705 HarfBuzz initialized successfully");
389
366
  return hbSingleton;
390
367
  } catch (err) {
391
368
  console.error("Failed to initialize HarfBuzz:", err);
@@ -896,7 +873,6 @@ async function buildDrawOps(p) {
896
873
  pixelRatio: p.canvas.pixelRatio,
897
874
  clear: true,
898
875
  bg: void 0
899
- // Background will be drawn as a separate layer with proper padding/border
900
876
  });
901
877
  if (p.lines.length === 0) return ops;
902
878
  const upem = Math.max(1, await p.getUnitsPerEm());
@@ -1024,7 +1000,12 @@ async function buildDrawOps(p) {
1024
1000
  }
1025
1001
  }
1026
1002
  if (gMinX !== Infinity) {
1027
- const gbox = { x: gMinX, y: gMinY, w: Math.max(1, gMaxX - gMinX), h: Math.max(1, gMaxY - gMinY) };
1003
+ const gbox = {
1004
+ x: gMinX,
1005
+ y: gMinY,
1006
+ w: Math.max(1, gMaxX - gMinX),
1007
+ h: Math.max(1, gMaxY - gMinY)
1008
+ };
1028
1009
  for (const op of textOps) {
1029
1010
  if (op.op === "FillPath" && !op.isShadow) {
1030
1011
  op.gradientBBox = gbox;
@@ -1032,14 +1013,16 @@ async function buildDrawOps(p) {
1032
1013
  }
1033
1014
  }
1034
1015
  if (p.background || p.border) {
1035
- const bgX = 0;
1036
- const bgY = 0;
1037
- const bgWidth = p.canvas.width;
1038
- const bgHeight = p.canvas.height;
1016
+ const contentWidth = p.contentRect?.width ?? p.canvas.width;
1017
+ const contentHeight = p.contentRect?.height ?? p.canvas.height;
1039
1018
  const borderWidth2 = p.border?.width ?? 0;
1040
1019
  const borderRadius = p.border?.radius ?? 0;
1041
1020
  const halfBorder = borderWidth2 / 2;
1042
- const maxRadius = Math.min(bgWidth - borderWidth2, bgHeight - borderWidth2) / 2;
1021
+ const canvasCenterX = p.canvas.width / 2;
1022
+ const canvasCenterY = p.canvas.height / 2;
1023
+ const bgX = canvasCenterX - contentWidth / 2;
1024
+ const bgY = canvasCenterY - contentHeight / 2;
1025
+ const maxRadius = Math.min(contentWidth - borderWidth2, contentHeight - borderWidth2) / 2;
1043
1026
  const outerRadius = Math.min(borderRadius, maxRadius);
1044
1027
  const innerRadius = Math.max(0, outerRadius - halfBorder);
1045
1028
  if (p.background?.color) {
@@ -1047,8 +1030,8 @@ async function buildDrawOps(p) {
1047
1030
  op: "Rectangle",
1048
1031
  x: bgX + borderWidth2,
1049
1032
  y: bgY + borderWidth2,
1050
- width: bgWidth - borderWidth2 * 2,
1051
- height: bgHeight - borderWidth2 * 2,
1033
+ width: contentWidth - borderWidth2 * 2,
1034
+ height: contentHeight - borderWidth2 * 2,
1052
1035
  fill: { kind: "solid", color: p.background.color, opacity: p.background.opacity },
1053
1036
  borderRadius: innerRadius
1054
1037
  });
@@ -1058,8 +1041,8 @@ async function buildDrawOps(p) {
1058
1041
  op: "RectangleStroke",
1059
1042
  x: bgX + halfBorder,
1060
1043
  y: bgY + halfBorder,
1061
- width: bgWidth - borderWidth2,
1062
- height: bgHeight - borderWidth2,
1044
+ width: contentWidth - borderWidth2,
1045
+ height: contentHeight - borderWidth2,
1063
1046
  stroke: {
1064
1047
  width: p.border.width,
1065
1048
  color: p.border.color,
@@ -2494,9 +2477,15 @@ async function createTextEngine(opts = {}) {
2494
2477
  emojiAvailable = true;
2495
2478
  } catch {
2496
2479
  }
2480
+ const padding2 = asset.padding ? typeof asset.padding === "number" ? {
2481
+ top: asset.padding,
2482
+ right: asset.padding,
2483
+ bottom: asset.padding,
2484
+ left: asset.padding
2485
+ } : asset.padding : { top: 0, right: 0, bottom: 0, left: 0 };
2497
2486
  lines = await layout.layout({
2498
2487
  text: asset.text,
2499
- width: asset.width ?? width,
2488
+ width: (asset.width ?? width) - padding2.left - padding2.right,
2500
2489
  letterSpacing: asset.style?.letterSpacing ?? 0,
2501
2490
  fontSize: main.size,
2502
2491
  lineHeight: asset.style?.lineHeight ?? 1.2,
@@ -2509,23 +2498,31 @@ async function createTextEngine(opts = {}) {
2509
2498
  `Failed to layout text: ${err instanceof Error ? err.message : String(err)}`
2510
2499
  );
2511
2500
  }
2512
- const padding = asset.padding ? typeof asset.padding === "number" ? { top: asset.padding, right: asset.padding, bottom: asset.padding, left: asset.padding } : asset.padding : { top: 0, right: 0, bottom: 0, left: 0 };
2501
+ const padding = asset.padding ? typeof asset.padding === "number" ? {
2502
+ top: asset.padding,
2503
+ right: asset.padding,
2504
+ bottom: asset.padding,
2505
+ left: asset.padding
2506
+ } : asset.padding : { top: 0, right: 0, bottom: 0, left: 0 };
2507
+ const borderWidth = asset.border?.width ?? 0;
2508
+ const canvasW = (asset.width ?? width) + borderWidth * 2;
2509
+ const canvasH = (asset.height ?? height) + borderWidth * 2;
2510
+ const canvasPR = pixelRatio;
2513
2511
  const textRect = {
2514
2512
  x: 0,
2515
2513
  y: 0,
2516
- width: asset.width ?? width,
2517
- height: asset.height ?? height
2514
+ width: (asset.width ?? width) - padding.left - padding.right,
2515
+ height: (asset.height ?? height) - padding.top - padding.bottom
2518
2516
  };
2519
- const borderWidth = asset.border?.width ?? 0;
2520
- const canvasW = (asset.width ?? width) + padding.left + padding.right + borderWidth * 2;
2521
- const canvasH = (asset.height ?? height) + padding.top + padding.bottom + borderWidth * 2;
2522
- const canvasPR = pixelRatio;
2517
+ const contentW = canvasW;
2518
+ const contentH = canvasH;
2523
2519
  let ops0;
2524
2520
  try {
2525
2521
  ops0 = await buildDrawOps({
2526
2522
  canvas: { width: canvasW, height: canvasH, pixelRatio: canvasPR },
2527
2523
  textRect,
2528
2524
  lines,
2525
+ contentRect: { width: contentW, height: contentH },
2529
2526
  font: {
2530
2527
  family: main.family,
2531
2528
  size: main.size,
@@ -2604,9 +2601,12 @@ async function createTextEngine(opts = {}) {
2604
2601
  console.log(
2605
2602
  `\u{1F3A8} Video settings: Animation=${hasAnimation}, Background=${hasBackground}, BorderRadius=${hasBorderRadius}, Alpha=${needsAlpha}`
2606
2603
  );
2604
+ const borderWidth = asset.border?.width ?? 0;
2605
+ const canvasWidth = (asset.width ?? width) + borderWidth * 2;
2606
+ const canvasHeight = (asset.height ?? height) + borderWidth * 2;
2607
2607
  const finalOptions = {
2608
- width: asset.width ?? width,
2609
- height: asset.height ?? height,
2608
+ width: canvasWidth,
2609
+ height: canvasHeight,
2610
2610
  fps,
2611
2611
  duration: options.duration ?? 3,
2612
2612
  outputPath: options.outputPath ?? "output.mp4",
@@ -157,13 +157,11 @@ function bufferToArrayBuffer(buffer) {
157
157
  var DEFAULT_WASM_URL = "https://shotstack-ingest-api-dev-sources.s3.ap-southeast-2.amazonaws.com/euo5r93oyr/zzz01k9h-yycyx-2x2y6-qx9bj-7n567b/source.wasm";
158
158
  async function fetchWasmFromUrl(url) {
159
159
  try {
160
- console.log(`\u{1F310} Fetching WASM from URL: ${url}`);
161
160
  const response = await fetch(url);
162
161
  if (response.ok) {
163
162
  const arrayBuffer = await response.arrayBuffer();
164
163
  const bytes = new Uint8Array(arrayBuffer);
165
164
  if (bytes.length >= 4 && bytes[0] === 0 && bytes[1] === 97 && bytes[2] === 115 && bytes[3] === 109) {
166
- console.log(`\u2705 Fetched WASM from URL (${bytes.length} bytes)`);
167
165
  return arrayBuffer;
168
166
  } else {
169
167
  console.error(`\u274C Invalid WASM magic number from URL: ${url}`);
@@ -211,21 +209,17 @@ async function loadWasmNode() {
211
209
  "/var/task/node_modules/harfbuzzjs/hb.wasm",
212
210
  "/var/task/node_modules/@shotstack/shotstack-canvas/assets/wasm/hb.wasm"
213
211
  );
214
- console.log(`\u{1F50D} Searching for WASM in ${candidates.length} local paths...`);
215
212
  for (const candidate of candidates) {
216
213
  try {
217
214
  const buffer = await readFile2(candidate);
218
- console.log(`\u2705 Found WASM at: ${candidate}`);
219
215
  return bufferToArrayBuffer(buffer);
220
216
  } catch {
221
217
  continue;
222
218
  }
223
219
  }
224
- console.log("\u{1F4C2} Local WASM not found, fetching from URL...");
225
220
  return await fetchWasmFromUrl(DEFAULT_WASM_URL);
226
221
  } catch (err) {
227
222
  console.error("Error in loadWasmNode:", err);
228
- console.log("\u26A0\uFE0F Error during local search, falling back to URL...");
229
223
  return await fetchWasmFromUrl(DEFAULT_WASM_URL);
230
224
  }
231
225
  }
@@ -233,13 +227,11 @@ async function loadWasmWeb(wasmBaseURL) {
233
227
  try {
234
228
  if (wasmBaseURL) {
235
229
  const url = wasmBaseURL.endsWith(".wasm") ? wasmBaseURL : wasmBaseURL.endsWith("/") ? `${wasmBaseURL}hb.wasm` : `${wasmBaseURL}/hb.wasm`;
236
- console.log(`Fetching WASM from: ${url}`);
237
230
  const response = await fetch(url);
238
231
  if (response.ok) {
239
232
  const arrayBuffer = await response.arrayBuffer();
240
233
  const bytes = new Uint8Array(arrayBuffer);
241
234
  if (bytes.length >= 4 && bytes[0] === 0 && bytes[1] === 97 && bytes[2] === 115 && bytes[3] === 109) {
242
- console.log(`\u2705 Valid WASM binary loaded (${bytes.length} bytes)`);
243
235
  return arrayBuffer;
244
236
  }
245
237
  }
@@ -255,7 +247,6 @@ function setupWasmInterceptors(wasmBinary) {
255
247
  window.fetch = function(input, init) {
256
248
  const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
257
249
  if (url.includes("hb.wasm") || url.endsWith(".wasm") && !url.includes("source.wasm")) {
258
- console.log(`\u{1F504} Intercepted fetch for: ${url}`);
259
250
  return Promise.resolve(
260
251
  new Response(wasmBinary.slice(0), {
261
252
  status: 200,
@@ -271,13 +262,9 @@ function setupWasmInterceptors(wasmBinary) {
271
262
  };
272
263
  const originalInstantiate = WebAssembly.instantiate;
273
264
  WebAssembly.instantiate = async function(bufferSourceOrModule, importObject) {
274
- console.log(
275
- `\u{1F504} WebAssembly.instantiate called, type: ${bufferSourceOrModule instanceof WebAssembly.Module ? "Module" : "BufferSource"}`
276
- );
277
265
  if (bufferSourceOrModule instanceof WebAssembly.Module) {
278
266
  return originalInstantiate.call(WebAssembly, bufferSourceOrModule, importObject);
279
267
  }
280
- console.log(`\u{1F504} Intercepted WebAssembly.instantiate, using pre-loaded WASM binary`);
281
268
  const module = await WebAssembly.compile(wasmBinary);
282
269
  const instance = await originalInstantiate.call(
283
270
  WebAssembly,
@@ -293,14 +280,12 @@ function setupWasmInterceptors(wasmBinary) {
293
280
  const response = await source;
294
281
  const url = response.url || "";
295
282
  if (url.includes("hb.wasm") || url.endsWith(".wasm") && !url.includes("source.wasm")) {
296
- console.log(`\u{1F504} Intercepted instantiateStreaming for: ${url}`);
297
283
  const module = await WebAssembly.compile(wasmBinary);
298
284
  const instance = await WebAssembly.instantiate(module, importObject);
299
285
  return { module, instance };
300
286
  }
301
287
  return originalInstantiateStreaming.call(WebAssembly, response, importObject);
302
288
  } catch (err) {
303
- console.log("\u{1F504} instantiateStreaming failed, using pre-loaded binary");
304
289
  const module = await WebAssembly.compile(wasmBinary);
305
290
  const instance = await WebAssembly.instantiate(module, importObject);
306
291
  return { module, instance };
@@ -320,33 +305,25 @@ async function initHB(wasmBaseURL) {
320
305
  if (!wasmBinary) {
321
306
  throw new Error("Failed to load WASM binary from any source");
322
307
  }
323
- console.log(`\u2705 WASM binary loaded successfully (${wasmBinary.byteLength} bytes)`);
324
308
  if (!isNode()) {
325
309
  setupWasmInterceptors(wasmBinary);
326
310
  window.Module = {
327
311
  wasmBinary,
328
312
  locateFile: (path) => {
329
- console.log(`\u{1F50D} locateFile called for: ${path}`);
330
313
  return path;
331
314
  }
332
315
  };
333
- console.log(`\u{1F30D} Set global Module.wasmBinary (${wasmBinary.byteLength} bytes)`);
334
316
  }
335
- console.log("\u{1F504} Importing harfbuzzjs/hb.js (factory)");
336
317
  const hbModule = await import("harfbuzzjs/hb.js");
337
318
  const hbFactory = hbModule.default || hbModule;
338
- console.log("\u{1F504} Calling hb factory with wasmBinary");
339
319
  const hbInstance = await hbFactory({ wasmBinary });
340
- console.log("\u{1F504} Importing harfbuzzjs/hbjs.js (wrapper)");
341
320
  const hbjsModule = await import("harfbuzzjs/hbjs.js");
342
321
  const hbjsWrapper = hbjsModule.default || hbjsModule;
343
- console.log("\u{1F504} Wrapping hb instance");
344
322
  const hb = hbjsWrapper(hbInstance);
345
323
  if (!hb || typeof hb.createBuffer !== "function" || typeof hb.createFont !== "function") {
346
324
  throw new Error("Failed to initialize HarfBuzz: unexpected export shape from 'harfbuzzjs'.");
347
325
  }
348
326
  hbSingleton = hb;
349
- console.log("\u2705 HarfBuzz initialized successfully");
350
327
  return hbSingleton;
351
328
  } catch (err) {
352
329
  console.error("Failed to initialize HarfBuzz:", err);
@@ -857,7 +834,6 @@ async function buildDrawOps(p) {
857
834
  pixelRatio: p.canvas.pixelRatio,
858
835
  clear: true,
859
836
  bg: void 0
860
- // Background will be drawn as a separate layer with proper padding/border
861
837
  });
862
838
  if (p.lines.length === 0) return ops;
863
839
  const upem = Math.max(1, await p.getUnitsPerEm());
@@ -985,7 +961,12 @@ async function buildDrawOps(p) {
985
961
  }
986
962
  }
987
963
  if (gMinX !== Infinity) {
988
- const gbox = { x: gMinX, y: gMinY, w: Math.max(1, gMaxX - gMinX), h: Math.max(1, gMaxY - gMinY) };
964
+ const gbox = {
965
+ x: gMinX,
966
+ y: gMinY,
967
+ w: Math.max(1, gMaxX - gMinX),
968
+ h: Math.max(1, gMaxY - gMinY)
969
+ };
989
970
  for (const op of textOps) {
990
971
  if (op.op === "FillPath" && !op.isShadow) {
991
972
  op.gradientBBox = gbox;
@@ -993,14 +974,16 @@ async function buildDrawOps(p) {
993
974
  }
994
975
  }
995
976
  if (p.background || p.border) {
996
- const bgX = 0;
997
- const bgY = 0;
998
- const bgWidth = p.canvas.width;
999
- const bgHeight = p.canvas.height;
977
+ const contentWidth = p.contentRect?.width ?? p.canvas.width;
978
+ const contentHeight = p.contentRect?.height ?? p.canvas.height;
1000
979
  const borderWidth2 = p.border?.width ?? 0;
1001
980
  const borderRadius = p.border?.radius ?? 0;
1002
981
  const halfBorder = borderWidth2 / 2;
1003
- const maxRadius = Math.min(bgWidth - borderWidth2, bgHeight - borderWidth2) / 2;
982
+ const canvasCenterX = p.canvas.width / 2;
983
+ const canvasCenterY = p.canvas.height / 2;
984
+ const bgX = canvasCenterX - contentWidth / 2;
985
+ const bgY = canvasCenterY - contentHeight / 2;
986
+ const maxRadius = Math.min(contentWidth - borderWidth2, contentHeight - borderWidth2) / 2;
1004
987
  const outerRadius = Math.min(borderRadius, maxRadius);
1005
988
  const innerRadius = Math.max(0, outerRadius - halfBorder);
1006
989
  if (p.background?.color) {
@@ -1008,8 +991,8 @@ async function buildDrawOps(p) {
1008
991
  op: "Rectangle",
1009
992
  x: bgX + borderWidth2,
1010
993
  y: bgY + borderWidth2,
1011
- width: bgWidth - borderWidth2 * 2,
1012
- height: bgHeight - borderWidth2 * 2,
994
+ width: contentWidth - borderWidth2 * 2,
995
+ height: contentHeight - borderWidth2 * 2,
1013
996
  fill: { kind: "solid", color: p.background.color, opacity: p.background.opacity },
1014
997
  borderRadius: innerRadius
1015
998
  });
@@ -1019,8 +1002,8 @@ async function buildDrawOps(p) {
1019
1002
  op: "RectangleStroke",
1020
1003
  x: bgX + halfBorder,
1021
1004
  y: bgY + halfBorder,
1022
- width: bgWidth - borderWidth2,
1023
- height: bgHeight - borderWidth2,
1005
+ width: contentWidth - borderWidth2,
1006
+ height: contentHeight - borderWidth2,
1024
1007
  stroke: {
1025
1008
  width: p.border.width,
1026
1009
  color: p.border.color,
@@ -2455,9 +2438,15 @@ async function createTextEngine(opts = {}) {
2455
2438
  emojiAvailable = true;
2456
2439
  } catch {
2457
2440
  }
2441
+ const padding2 = asset.padding ? typeof asset.padding === "number" ? {
2442
+ top: asset.padding,
2443
+ right: asset.padding,
2444
+ bottom: asset.padding,
2445
+ left: asset.padding
2446
+ } : asset.padding : { top: 0, right: 0, bottom: 0, left: 0 };
2458
2447
  lines = await layout.layout({
2459
2448
  text: asset.text,
2460
- width: asset.width ?? width,
2449
+ width: (asset.width ?? width) - padding2.left - padding2.right,
2461
2450
  letterSpacing: asset.style?.letterSpacing ?? 0,
2462
2451
  fontSize: main.size,
2463
2452
  lineHeight: asset.style?.lineHeight ?? 1.2,
@@ -2470,23 +2459,31 @@ async function createTextEngine(opts = {}) {
2470
2459
  `Failed to layout text: ${err instanceof Error ? err.message : String(err)}`
2471
2460
  );
2472
2461
  }
2473
- const padding = asset.padding ? typeof asset.padding === "number" ? { top: asset.padding, right: asset.padding, bottom: asset.padding, left: asset.padding } : asset.padding : { top: 0, right: 0, bottom: 0, left: 0 };
2462
+ const padding = asset.padding ? typeof asset.padding === "number" ? {
2463
+ top: asset.padding,
2464
+ right: asset.padding,
2465
+ bottom: asset.padding,
2466
+ left: asset.padding
2467
+ } : asset.padding : { top: 0, right: 0, bottom: 0, left: 0 };
2468
+ const borderWidth = asset.border?.width ?? 0;
2469
+ const canvasW = (asset.width ?? width) + borderWidth * 2;
2470
+ const canvasH = (asset.height ?? height) + borderWidth * 2;
2471
+ const canvasPR = pixelRatio;
2474
2472
  const textRect = {
2475
2473
  x: 0,
2476
2474
  y: 0,
2477
- width: asset.width ?? width,
2478
- height: asset.height ?? height
2475
+ width: (asset.width ?? width) - padding.left - padding.right,
2476
+ height: (asset.height ?? height) - padding.top - padding.bottom
2479
2477
  };
2480
- const borderWidth = asset.border?.width ?? 0;
2481
- const canvasW = (asset.width ?? width) + padding.left + padding.right + borderWidth * 2;
2482
- const canvasH = (asset.height ?? height) + padding.top + padding.bottom + borderWidth * 2;
2483
- const canvasPR = pixelRatio;
2478
+ const contentW = canvasW;
2479
+ const contentH = canvasH;
2484
2480
  let ops0;
2485
2481
  try {
2486
2482
  ops0 = await buildDrawOps({
2487
2483
  canvas: { width: canvasW, height: canvasH, pixelRatio: canvasPR },
2488
2484
  textRect,
2489
2485
  lines,
2486
+ contentRect: { width: contentW, height: contentH },
2490
2487
  font: {
2491
2488
  family: main.family,
2492
2489
  size: main.size,
@@ -2565,9 +2562,12 @@ async function createTextEngine(opts = {}) {
2565
2562
  console.log(
2566
2563
  `\u{1F3A8} Video settings: Animation=${hasAnimation}, Background=${hasBackground}, BorderRadius=${hasBorderRadius}, Alpha=${needsAlpha}`
2567
2564
  );
2565
+ const borderWidth = asset.border?.width ?? 0;
2566
+ const canvasWidth = (asset.width ?? width) + borderWidth * 2;
2567
+ const canvasHeight = (asset.height ?? height) + borderWidth * 2;
2568
2568
  const finalOptions = {
2569
- width: asset.width ?? width,
2570
- height: asset.height ?? height,
2569
+ width: canvasWidth,
2570
+ height: canvasHeight,
2571
2571
  fps,
2572
2572
  duration: options.duration ?? 3,
2573
2573
  outputPath: options.outputPath ?? "output.mp4",
package/dist/entry.web.js CHANGED
@@ -161,13 +161,11 @@ function bufferToArrayBuffer(buffer) {
161
161
  var DEFAULT_WASM_URL = "https://shotstack-ingest-api-dev-sources.s3.ap-southeast-2.amazonaws.com/euo5r93oyr/zzz01k9h-yycyx-2x2y6-qx9bj-7n567b/source.wasm";
162
162
  async function fetchWasmFromUrl(url) {
163
163
  try {
164
- console.log(`\u{1F310} Fetching WASM from URL: ${url}`);
165
164
  const response = await fetch(url);
166
165
  if (response.ok) {
167
166
  const arrayBuffer = await response.arrayBuffer();
168
167
  const bytes = new Uint8Array(arrayBuffer);
169
168
  if (bytes.length >= 4 && bytes[0] === 0 && bytes[1] === 97 && bytes[2] === 115 && bytes[3] === 109) {
170
- console.log(`\u2705 Fetched WASM from URL (${bytes.length} bytes)`);
171
169
  return arrayBuffer;
172
170
  } else {
173
171
  console.error(`\u274C Invalid WASM magic number from URL: ${url}`);
@@ -215,21 +213,17 @@ async function loadWasmNode() {
215
213
  "/var/task/node_modules/harfbuzzjs/hb.wasm",
216
214
  "/var/task/node_modules/@shotstack/shotstack-canvas/assets/wasm/hb.wasm"
217
215
  );
218
- console.log(`\u{1F50D} Searching for WASM in ${candidates.length} local paths...`);
219
216
  for (const candidate of candidates) {
220
217
  try {
221
218
  const buffer = await readFile(candidate);
222
- console.log(`\u2705 Found WASM at: ${candidate}`);
223
219
  return bufferToArrayBuffer(buffer);
224
220
  } catch {
225
221
  continue;
226
222
  }
227
223
  }
228
- console.log("\u{1F4C2} Local WASM not found, fetching from URL...");
229
224
  return await fetchWasmFromUrl(DEFAULT_WASM_URL);
230
225
  } catch (err) {
231
226
  console.error("Error in loadWasmNode:", err);
232
- console.log("\u26A0\uFE0F Error during local search, falling back to URL...");
233
227
  return await fetchWasmFromUrl(DEFAULT_WASM_URL);
234
228
  }
235
229
  }
@@ -237,13 +231,11 @@ async function loadWasmWeb(wasmBaseURL) {
237
231
  try {
238
232
  if (wasmBaseURL) {
239
233
  const url = wasmBaseURL.endsWith(".wasm") ? wasmBaseURL : wasmBaseURL.endsWith("/") ? `${wasmBaseURL}hb.wasm` : `${wasmBaseURL}/hb.wasm`;
240
- console.log(`Fetching WASM from: ${url}`);
241
234
  const response = await fetch(url);
242
235
  if (response.ok) {
243
236
  const arrayBuffer = await response.arrayBuffer();
244
237
  const bytes = new Uint8Array(arrayBuffer);
245
238
  if (bytes.length >= 4 && bytes[0] === 0 && bytes[1] === 97 && bytes[2] === 115 && bytes[3] === 109) {
246
- console.log(`\u2705 Valid WASM binary loaded (${bytes.length} bytes)`);
247
239
  return arrayBuffer;
248
240
  }
249
241
  }
@@ -259,7 +251,6 @@ function setupWasmInterceptors(wasmBinary) {
259
251
  window.fetch = function(input, init) {
260
252
  const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
261
253
  if (url.includes("hb.wasm") || url.endsWith(".wasm") && !url.includes("source.wasm")) {
262
- console.log(`\u{1F504} Intercepted fetch for: ${url}`);
263
254
  return Promise.resolve(
264
255
  new Response(wasmBinary.slice(0), {
265
256
  status: 200,
@@ -275,13 +266,9 @@ function setupWasmInterceptors(wasmBinary) {
275
266
  };
276
267
  const originalInstantiate = WebAssembly.instantiate;
277
268
  WebAssembly.instantiate = async function(bufferSourceOrModule, importObject) {
278
- console.log(
279
- `\u{1F504} WebAssembly.instantiate called, type: ${bufferSourceOrModule instanceof WebAssembly.Module ? "Module" : "BufferSource"}`
280
- );
281
269
  if (bufferSourceOrModule instanceof WebAssembly.Module) {
282
270
  return originalInstantiate.call(WebAssembly, bufferSourceOrModule, importObject);
283
271
  }
284
- console.log(`\u{1F504} Intercepted WebAssembly.instantiate, using pre-loaded WASM binary`);
285
272
  const module = await WebAssembly.compile(wasmBinary);
286
273
  const instance = await originalInstantiate.call(
287
274
  WebAssembly,
@@ -297,14 +284,12 @@ function setupWasmInterceptors(wasmBinary) {
297
284
  const response = await source;
298
285
  const url = response.url || "";
299
286
  if (url.includes("hb.wasm") || url.endsWith(".wasm") && !url.includes("source.wasm")) {
300
- console.log(`\u{1F504} Intercepted instantiateStreaming for: ${url}`);
301
287
  const module = await WebAssembly.compile(wasmBinary);
302
288
  const instance = await WebAssembly.instantiate(module, importObject);
303
289
  return { module, instance };
304
290
  }
305
291
  return originalInstantiateStreaming.call(WebAssembly, response, importObject);
306
292
  } catch (err) {
307
- console.log("\u{1F504} instantiateStreaming failed, using pre-loaded binary");
308
293
  const module = await WebAssembly.compile(wasmBinary);
309
294
  const instance = await WebAssembly.instantiate(module, importObject);
310
295
  return { module, instance };
@@ -324,33 +309,25 @@ async function initHB(wasmBaseURL) {
324
309
  if (!wasmBinary) {
325
310
  throw new Error("Failed to load WASM binary from any source");
326
311
  }
327
- console.log(`\u2705 WASM binary loaded successfully (${wasmBinary.byteLength} bytes)`);
328
312
  if (!isNode()) {
329
313
  setupWasmInterceptors(wasmBinary);
330
314
  window.Module = {
331
315
  wasmBinary,
332
316
  locateFile: (path) => {
333
- console.log(`\u{1F50D} locateFile called for: ${path}`);
334
317
  return path;
335
318
  }
336
319
  };
337
- console.log(`\u{1F30D} Set global Module.wasmBinary (${wasmBinary.byteLength} bytes)`);
338
320
  }
339
- console.log("\u{1F504} Importing harfbuzzjs/hb.js (factory)");
340
321
  const hbModule = await import("./hb-ODWKSLMB.js");
341
322
  const hbFactory = hbModule.default || hbModule;
342
- console.log("\u{1F504} Calling hb factory with wasmBinary");
343
323
  const hbInstance = await hbFactory({ wasmBinary });
344
- console.log("\u{1F504} Importing harfbuzzjs/hbjs.js (wrapper)");
345
324
  const hbjsModule = await import("./hbjs-HHU2TAW7.js");
346
325
  const hbjsWrapper = hbjsModule.default || hbjsModule;
347
- console.log("\u{1F504} Wrapping hb instance");
348
326
  const hb = hbjsWrapper(hbInstance);
349
327
  if (!hb || typeof hb.createBuffer !== "function" || typeof hb.createFont !== "function") {
350
328
  throw new Error("Failed to initialize HarfBuzz: unexpected export shape from 'harfbuzzjs'.");
351
329
  }
352
330
  hbSingleton = hb;
353
- console.log("\u2705 HarfBuzz initialized successfully");
354
331
  return hbSingleton;
355
332
  } catch (err) {
356
333
  console.error("Failed to initialize HarfBuzz:", err);
@@ -862,7 +839,6 @@ async function buildDrawOps(p) {
862
839
  pixelRatio: p.canvas.pixelRatio,
863
840
  clear: true,
864
841
  bg: void 0
865
- // Background will be drawn as a separate layer with proper padding/border
866
842
  });
867
843
  if (p.lines.length === 0) return ops;
868
844
  const upem = Math.max(1, await p.getUnitsPerEm());
@@ -990,7 +966,12 @@ async function buildDrawOps(p) {
990
966
  }
991
967
  }
992
968
  if (gMinX !== Infinity) {
993
- const gbox = { x: gMinX, y: gMinY, w: Math.max(1, gMaxX - gMinX), h: Math.max(1, gMaxY - gMinY) };
969
+ const gbox = {
970
+ x: gMinX,
971
+ y: gMinY,
972
+ w: Math.max(1, gMaxX - gMinX),
973
+ h: Math.max(1, gMaxY - gMinY)
974
+ };
994
975
  for (const op of textOps) {
995
976
  if (op.op === "FillPath" && !op.isShadow) {
996
977
  op.gradientBBox = gbox;
@@ -998,14 +979,16 @@ async function buildDrawOps(p) {
998
979
  }
999
980
  }
1000
981
  if (p.background || p.border) {
1001
- const bgX = 0;
1002
- const bgY = 0;
1003
- const bgWidth = p.canvas.width;
1004
- const bgHeight = p.canvas.height;
982
+ const contentWidth = p.contentRect?.width ?? p.canvas.width;
983
+ const contentHeight = p.contentRect?.height ?? p.canvas.height;
1005
984
  const borderWidth2 = p.border?.width ?? 0;
1006
985
  const borderRadius = p.border?.radius ?? 0;
1007
986
  const halfBorder = borderWidth2 / 2;
1008
- const maxRadius = Math.min(bgWidth - borderWidth2, bgHeight - borderWidth2) / 2;
987
+ const canvasCenterX = p.canvas.width / 2;
988
+ const canvasCenterY = p.canvas.height / 2;
989
+ const bgX = canvasCenterX - contentWidth / 2;
990
+ const bgY = canvasCenterY - contentHeight / 2;
991
+ const maxRadius = Math.min(contentWidth - borderWidth2, contentHeight - borderWidth2) / 2;
1009
992
  const outerRadius = Math.min(borderRadius, maxRadius);
1010
993
  const innerRadius = Math.max(0, outerRadius - halfBorder);
1011
994
  if (p.background?.color) {
@@ -1013,8 +996,8 @@ async function buildDrawOps(p) {
1013
996
  op: "Rectangle",
1014
997
  x: bgX + borderWidth2,
1015
998
  y: bgY + borderWidth2,
1016
- width: bgWidth - borderWidth2 * 2,
1017
- height: bgHeight - borderWidth2 * 2,
999
+ width: contentWidth - borderWidth2 * 2,
1000
+ height: contentHeight - borderWidth2 * 2,
1018
1001
  fill: { kind: "solid", color: p.background.color, opacity: p.background.opacity },
1019
1002
  borderRadius: innerRadius
1020
1003
  });
@@ -1024,8 +1007,8 @@ async function buildDrawOps(p) {
1024
1007
  op: "RectangleStroke",
1025
1008
  x: bgX + halfBorder,
1026
1009
  y: bgY + halfBorder,
1027
- width: bgWidth - borderWidth2,
1028
- height: bgHeight - borderWidth2,
1010
+ width: contentWidth - borderWidth2,
1011
+ height: contentHeight - borderWidth2,
1029
1012
  stroke: {
1030
1013
  width: p.border.width,
1031
1014
  color: p.border.color,
@@ -2166,9 +2149,10 @@ async function createTextEngine(opts = {}) {
2166
2149
  emojiAvailable = true;
2167
2150
  } catch {
2168
2151
  }
2152
+ const padding2 = asset.padding ? typeof asset.padding === "number" ? { top: asset.padding, right: asset.padding, bottom: asset.padding, left: asset.padding } : asset.padding : { top: 0, right: 0, bottom: 0, left: 0 };
2169
2153
  lines = await layout.layout({
2170
2154
  text: asset.text,
2171
- width: asset.width ?? width,
2155
+ width: (asset.width ?? width) - padding2.left - padding2.right,
2172
2156
  letterSpacing: asset.style?.letterSpacing ?? 0,
2173
2157
  fontSize: main.size,
2174
2158
  lineHeight: asset.style?.lineHeight ?? 1.2,
@@ -2182,22 +2166,25 @@ async function createTextEngine(opts = {}) {
2182
2166
  );
2183
2167
  }
2184
2168
  const padding = asset.padding ? typeof asset.padding === "number" ? { top: asset.padding, right: asset.padding, bottom: asset.padding, left: asset.padding } : asset.padding : { top: 0, right: 0, bottom: 0, left: 0 };
2169
+ const borderWidth = asset.border?.width ?? 0;
2170
+ const canvasW = (asset.width ?? width) + borderWidth * 2;
2171
+ const canvasH = (asset.height ?? height) + borderWidth * 2;
2172
+ const canvasPR = pixelRatio;
2185
2173
  const textRect = {
2186
2174
  x: 0,
2187
2175
  y: 0,
2188
- width: asset.width ?? width,
2189
- height: asset.height ?? height
2176
+ width: (asset.width ?? width) - padding.left - padding.right,
2177
+ height: (asset.height ?? height) - padding.top - padding.bottom
2190
2178
  };
2191
- const borderWidth = asset.border?.width ?? 0;
2192
- const canvasW = (asset.width ?? width) + padding.left + padding.right + borderWidth * 2;
2193
- const canvasH = (asset.height ?? height) + padding.top + padding.bottom + borderWidth * 2;
2194
- const canvasPR = pixelRatio;
2179
+ const contentW = canvasW;
2180
+ const contentH = canvasH;
2195
2181
  let ops0;
2196
2182
  try {
2197
2183
  ops0 = await buildDrawOps({
2198
2184
  canvas: { width: canvasW, height: canvasH, pixelRatio: canvasPR },
2199
2185
  textRect,
2200
2186
  lines,
2187
+ contentRect: { width: contentW, height: contentH },
2201
2188
  font: {
2202
2189
  family: main.family,
2203
2190
  size: main.size,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shotstack/shotstack-canvas",
3
- "version": "1.6.1",
3
+ "version": "1.6.3",
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",