@shotstack/shotstack-canvas 1.4.6 → 1.5.0

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.
@@ -94,11 +94,26 @@ var animationSchema = Joi.object({
94
94
  otherwise: Joi.forbidden()
95
95
  })
96
96
  }).unknown(false);
97
+ var borderSchema = Joi.object({
98
+ width: Joi.number().min(0).default(0),
99
+ color: Joi.string().pattern(HEX6).default("#000000"),
100
+ opacity: Joi.number().min(0).max(1).default(1)
101
+ }).unknown(false);
97
102
  var backgroundSchema = Joi.object({
98
103
  color: Joi.string().pattern(HEX6).optional(),
99
104
  opacity: Joi.number().min(0).max(1).default(1),
100
- borderRadius: Joi.number().min(0).default(0)
105
+ borderRadius: Joi.number().min(0).default(0),
106
+ border: borderSchema.optional()
101
107
  }).unknown(false);
108
+ var paddingSchema = Joi.alternatives().try(
109
+ Joi.number().min(0).default(0),
110
+ Joi.object({
111
+ top: Joi.number().min(0).default(0),
112
+ right: Joi.number().min(0).default(0),
113
+ bottom: Joi.number().min(0).default(0),
114
+ left: Joi.number().min(0).default(0)
115
+ }).unknown(false)
116
+ );
102
117
  var customFontSchema = Joi.object({
103
118
  src: Joi.string().uri().required(),
104
119
  family: Joi.string().required(),
@@ -116,6 +131,7 @@ var RichTextAssetSchema = Joi.object({
116
131
  stroke: strokeSchema.optional(),
117
132
  shadow: shadowSchema.optional(),
118
133
  background: backgroundSchema.optional(),
134
+ padding: paddingSchema.optional(),
119
135
  align: alignmentSchema.optional(),
120
136
  animation: animationSchema.optional(),
121
137
  customFonts: Joi.array().items(customFontSchema).optional(),
@@ -254,6 +270,23 @@ function setupWasmInterceptors(wasmBinary) {
254
270
  }
255
271
  return originalFetch.apply(this, [input, init]);
256
272
  };
273
+ const originalInstantiate = WebAssembly.instantiate;
274
+ WebAssembly.instantiate = async function(bufferSourceOrModule, importObject) {
275
+ console.log(
276
+ `\u{1F504} WebAssembly.instantiate called, type: ${bufferSourceOrModule instanceof WebAssembly.Module ? "Module" : "BufferSource"}`
277
+ );
278
+ if (bufferSourceOrModule instanceof WebAssembly.Module) {
279
+ return originalInstantiate.call(WebAssembly, bufferSourceOrModule, importObject);
280
+ }
281
+ console.log(`\u{1F504} Intercepted WebAssembly.instantiate, using pre-loaded WASM binary`);
282
+ const module = await WebAssembly.compile(wasmBinary);
283
+ const instance = await originalInstantiate.call(
284
+ WebAssembly,
285
+ module,
286
+ importObject
287
+ );
288
+ return { module, instance };
289
+ };
257
290
  const originalInstantiateStreaming = WebAssembly.instantiateStreaming;
258
291
  if (originalInstantiateStreaming) {
259
292
  WebAssembly.instantiateStreaming = async function(source, importObject) {
@@ -291,21 +324,25 @@ async function initHB(wasmBaseURL) {
291
324
  console.log(`\u2705 WASM binary loaded successfully (${wasmBinary.byteLength} bytes)`);
292
325
  if (!isNode()) {
293
326
  setupWasmInterceptors(wasmBinary);
294
- }
295
- const mod = await import("harfbuzzjs");
296
- let hb;
297
- const candidate = mod.default || mod;
298
- if (typeof candidate === "function") {
299
- hb = await candidate({
300
- wasmBinary
301
- });
302
- } else if (candidate && typeof candidate.then === "function") {
303
- hb = await candidate;
304
- } else if (candidate && typeof candidate.createBuffer === "function") {
305
- hb = candidate;
306
- } else {
307
- throw new Error(`Unexpected harfbuzzjs export type: ${typeof candidate}`);
308
- }
327
+ window.Module = {
328
+ wasmBinary,
329
+ locateFile: (path) => {
330
+ console.log(`\u{1F50D} locateFile called for: ${path}`);
331
+ return path;
332
+ }
333
+ };
334
+ console.log(`\u{1F30D} Set global Module.wasmBinary (${wasmBinary.byteLength} bytes)`);
335
+ }
336
+ console.log("\u{1F504} Importing harfbuzzjs/hb.js (factory)");
337
+ const hbModule = await import("harfbuzzjs/hb.js");
338
+ const hbFactory = hbModule.default || hbModule;
339
+ console.log("\u{1F504} Calling hb factory with wasmBinary");
340
+ const hbInstance = await hbFactory({ wasmBinary });
341
+ console.log("\u{1F504} Importing harfbuzzjs/hbjs.js (wrapper)");
342
+ const hbjsModule = await import("harfbuzzjs/hbjs.js");
343
+ const hbjsWrapper = hbjsModule.default || hbjsModule;
344
+ console.log("\u{1F504} Wrapping hb instance");
345
+ const hb = hbjsWrapper(hbInstance);
309
346
  if (!hb || typeof hb.createBuffer !== "function" || typeof hb.createFont !== "function") {
310
347
  throw new Error("Failed to initialize HarfBuzz: unexpected export shape from 'harfbuzzjs'.");
311
348
  }
@@ -768,15 +805,27 @@ function decorationGeometry(kind, p) {
768
805
  }
769
806
 
770
807
  // src/core/drawops.ts
808
+ function normalizePadding(padding) {
809
+ if (padding === void 0 || padding === null) {
810
+ return { top: 0, right: 0, bottom: 0, left: 0 };
811
+ }
812
+ if (typeof padding === "number") {
813
+ return { top: padding, right: padding, bottom: padding, left: padding };
814
+ }
815
+ return padding;
816
+ }
771
817
  async function buildDrawOps(p) {
772
818
  const ops = [];
819
+ const padding = normalizePadding(p.padding);
820
+ const borderWidth = p.background?.border?.width ?? 0;
773
821
  ops.push({
774
822
  op: "BeginFrame",
775
823
  width: p.canvas.width,
776
824
  height: p.canvas.height,
777
825
  pixelRatio: p.canvas.pixelRatio,
778
826
  clear: true,
779
- bg: p.background && p.background.color ? { color: p.background.color, opacity: p.background.opacity, radius: p.background.borderRadius } : void 0
827
+ bg: void 0
828
+ // Background will be drawn as a separate layer with proper padding/border
780
829
  });
781
830
  if (p.lines.length === 0) return ops;
782
831
  const upem = Math.max(1, await p.getUnitsPerEm());
@@ -786,33 +835,34 @@ async function buildDrawOps(p) {
786
835
  let blockY;
787
836
  switch (p.align.vertical) {
788
837
  case "top":
789
- blockY = p.font.size;
838
+ blockY = p.font.size + padding.top;
790
839
  break;
791
840
  case "bottom":
792
- blockY = p.textRect.height - (numLines - 1) * lineHeightPx;
841
+ blockY = p.textRect.height - (numLines - 1) * lineHeightPx + padding.top;
793
842
  break;
794
843
  case "middle":
795
844
  default:
796
845
  const capHeightRatio = 0.35;
797
846
  const visualOffset = p.font.size * capHeightRatio;
798
- blockY = (p.textRect.height - (numLines - 1) * lineHeightPx) / 2 + visualOffset;
847
+ blockY = (p.textRect.height - (numLines - 1) * lineHeightPx) / 2 + visualOffset + padding.top;
799
848
  break;
800
849
  }
801
850
  const fill = p.style.gradient ? gradientSpecFrom(p.style.gradient, 1) : { kind: "solid", color: p.font.color, opacity: p.font.opacity };
802
851
  const decoColor = p.style.gradient ? p.style.gradient.stops[p.style.gradient.stops.length - 1]?.color ?? p.font.color : p.font.color;
852
+ const textOps = [];
803
853
  let gMinX = Infinity, gMinY = Infinity, gMaxX = -Infinity, gMaxY = -Infinity;
804
854
  for (const line of p.lines) {
805
855
  let lineX;
806
856
  switch (p.align.horizontal) {
807
857
  case "left":
808
- lineX = 0;
858
+ lineX = padding.left;
809
859
  break;
810
860
  case "right":
811
- lineX = p.textRect.width - line.width;
861
+ lineX = p.textRect.width - line.width + padding.left;
812
862
  break;
813
863
  case "center":
814
864
  default:
815
- lineX = (p.textRect.width - line.width) / 2;
865
+ lineX = (p.textRect.width - line.width) / 2 + padding.left;
816
866
  break;
817
867
  }
818
868
  let xCursor = lineX;
@@ -836,7 +886,7 @@ async function buildDrawOps(p) {
836
886
  if (x2 > gMaxX) gMaxX = x2;
837
887
  if (y2 > gMaxY) gMaxY = y2;
838
888
  if (p.shadow && p.shadow.blur > 0) {
839
- ops.push({
889
+ textOps.push({
840
890
  isShadow: true,
841
891
  op: "FillPath",
842
892
  path,
@@ -847,7 +897,7 @@ async function buildDrawOps(p) {
847
897
  });
848
898
  }
849
899
  if (p.stroke && p.stroke.width > 0) {
850
- ops.push({
900
+ textOps.push({
851
901
  op: "StrokePath",
852
902
  path,
853
903
  x: glyphX,
@@ -858,7 +908,7 @@ async function buildDrawOps(p) {
858
908
  opacity: p.stroke.opacity
859
909
  });
860
910
  }
861
- ops.push({
911
+ textOps.push({
862
912
  op: "FillPath",
863
913
  path,
864
914
  x: glyphX,
@@ -875,7 +925,7 @@ async function buildDrawOps(p) {
875
925
  lineWidth: line.width,
876
926
  xStart: lineX
877
927
  });
878
- ops.push({
928
+ textOps.push({
879
929
  op: "DecorationLine",
880
930
  from: { x: deco.x1, y: deco.y },
881
931
  to: { x: deco.x2, y: deco.y },
@@ -887,12 +937,46 @@ async function buildDrawOps(p) {
887
937
  }
888
938
  if (gMinX !== Infinity) {
889
939
  const gbox = { x: gMinX, y: gMinY, w: Math.max(1, gMaxX - gMinX), h: Math.max(1, gMaxY - gMinY) };
890
- for (const op of ops) {
940
+ for (const op of textOps) {
891
941
  if (op.op === "FillPath" && !op.isShadow) {
892
942
  op.gradientBBox = gbox;
893
943
  }
894
944
  }
895
945
  }
946
+ if (p.background && (p.background.color || p.background.border)) {
947
+ const bgX = 0;
948
+ const bgY = 0;
949
+ const bgWidth = p.canvas.width;
950
+ const bgHeight = p.canvas.height;
951
+ if (p.background.color) {
952
+ ops.push({
953
+ op: "Rectangle",
954
+ x: bgX,
955
+ y: bgY,
956
+ width: bgWidth,
957
+ height: bgHeight,
958
+ fill: { kind: "solid", color: p.background.color, opacity: p.background.opacity },
959
+ borderRadius: p.background.borderRadius
960
+ });
961
+ }
962
+ if (p.background.border && p.background.border.width > 0) {
963
+ const halfBorder = p.background.border.width / 2;
964
+ ops.push({
965
+ op: "RectangleStroke",
966
+ x: bgX + halfBorder,
967
+ y: bgY + halfBorder,
968
+ width: bgWidth - p.background.border.width,
969
+ height: bgHeight - p.background.border.width,
970
+ stroke: {
971
+ width: p.background.border.width,
972
+ color: p.background.border.color,
973
+ opacity: p.background.border.opacity
974
+ },
975
+ borderRadius: Math.max(0, p.background.borderRadius - halfBorder)
976
+ });
977
+ }
978
+ }
979
+ ops.push(...textOps);
896
980
  return ops;
897
981
  }
898
982
  function tokenizePath(d) {
@@ -1702,6 +1786,44 @@ async function createNodePainter(opts) {
1702
1786
  });
1703
1787
  continue;
1704
1788
  }
1789
+ if (op.op === "Rectangle") {
1790
+ renderToBoth((context) => {
1791
+ context.save();
1792
+ const fill = makeGradientFromBBox(context, op.fill, {
1793
+ x: op.x,
1794
+ y: op.y,
1795
+ w: op.width,
1796
+ h: op.height
1797
+ });
1798
+ context.fillStyle = fill;
1799
+ if (op.borderRadius && op.borderRadius > 0) {
1800
+ context.beginPath();
1801
+ roundRectPath(context, op.x, op.y, op.width, op.height, op.borderRadius);
1802
+ context.fill();
1803
+ } else {
1804
+ context.fillRect(op.x, op.y, op.width, op.height);
1805
+ }
1806
+ context.restore();
1807
+ });
1808
+ continue;
1809
+ }
1810
+ if (op.op === "RectangleStroke") {
1811
+ renderToBoth((context) => {
1812
+ context.save();
1813
+ const c = parseHex6(op.stroke.color, op.stroke.opacity);
1814
+ context.strokeStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
1815
+ context.lineWidth = op.stroke.width;
1816
+ if (op.borderRadius && op.borderRadius > 0) {
1817
+ context.beginPath();
1818
+ roundRectPath(context, op.x, op.y, op.width, op.height, op.borderRadius);
1819
+ context.stroke();
1820
+ } else {
1821
+ context.strokeRect(op.x, op.y, op.width, op.height);
1822
+ }
1823
+ context.restore();
1824
+ });
1825
+ continue;
1826
+ }
1705
1827
  }
1706
1828
  if (needsAlphaExtraction) {
1707
1829
  const whiteData = ctx.getImageData(0, 0, canvas.width, canvas.height);
@@ -2265,14 +2387,15 @@ async function createTextEngine(opts = {}) {
2265
2387
  `Failed to layout text: ${err instanceof Error ? err.message : String(err)}`
2266
2388
  );
2267
2389
  }
2390
+ 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 };
2268
2391
  const textRect = {
2269
2392
  x: 0,
2270
2393
  y: 0,
2271
2394
  width: asset.width ?? width,
2272
2395
  height: asset.height ?? height
2273
2396
  };
2274
- const canvasW = asset.width ?? width;
2275
- const canvasH = asset.height ?? height;
2397
+ const canvasW = (asset.width ?? width) + padding.left + padding.right;
2398
+ const canvasH = (asset.height ?? height) + padding.top + padding.bottom;
2276
2399
  const canvasPR = asset.pixelRatio ?? pixelRatio;
2277
2400
  let ops0;
2278
2401
  try {
@@ -2299,6 +2422,7 @@ async function createTextEngine(opts = {}) {
2299
2422
  vertical: asset.align?.vertical ?? "middle"
2300
2423
  },
2301
2424
  background: asset.background,
2425
+ padding: asset.padding,
2302
2426
  glyphPathProvider: (gid, fontDesc) => fonts.glyphPath(fontDesc || desc, gid),
2303
2427
  getUnitsPerEm: (fontDesc) => fonts.getUnitsPerEm(fontDesc || desc)
2304
2428
  });
@@ -64,6 +64,17 @@ type RichTextValidated = Required<{
64
64
  color?: string;
65
65
  opacity: number;
66
66
  borderRadius: number;
67
+ border?: {
68
+ width: number;
69
+ color: string;
70
+ opacity: number;
71
+ };
72
+ };
73
+ padding?: number | {
74
+ top: number;
75
+ right: number;
76
+ bottom: number;
77
+ left: number;
67
78
  };
68
79
  align?: {
69
80
  horizontal: "left" | "center" | "right";
@@ -177,6 +188,26 @@ type DrawOp = {
177
188
  width: number;
178
189
  color: string;
179
190
  opacity: number;
191
+ } | {
192
+ op: "Rectangle";
193
+ x: number;
194
+ y: number;
195
+ width: number;
196
+ height: number;
197
+ fill: GradientSpec;
198
+ borderRadius?: number;
199
+ } | {
200
+ op: "RectangleStroke";
201
+ x: number;
202
+ y: number;
203
+ width: number;
204
+ height: number;
205
+ stroke: {
206
+ width: number;
207
+ color: string;
208
+ opacity: number;
209
+ };
210
+ borderRadius?: number;
180
211
  };
181
212
  type EngineInit = {
182
213
  width: number;