@shotstack/shotstack-canvas 1.1.6 → 1.1.8

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;
@@ -1214,6 +1268,7 @@ async function createNodePainter(opts) {
1214
1268
  const hasBackground = !!(op.bg && op.bg.color);
1215
1269
  needsAlphaExtraction = !hasBackground;
1216
1270
  if (op.bg && op.bg.color) {
1271
+ ctx.clearRect(0, 0, op.width, op.height);
1217
1272
  const { color, opacity, radius } = op.bg;
1218
1273
  const c = parseHex6(color, opacity);
1219
1274
  ctx.fillStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
@@ -1227,10 +1282,15 @@ async function createNodePainter(opts) {
1227
1282
  ctx.fillRect(0, 0, op.width, op.height);
1228
1283
  }
1229
1284
  } else {
1285
+ ctx.clearRect(0, 0, op.width, op.height);
1286
+ ctx.save();
1230
1287
  ctx.fillStyle = "rgb(255, 255, 255)";
1231
1288
  ctx.fillRect(0, 0, op.width, op.height);
1289
+ ctx.restore();
1290
+ offscreenCtx.save();
1232
1291
  offscreenCtx.fillStyle = "rgb(0, 0, 0)";
1233
1292
  offscreenCtx.fillRect(0, 0, op.width, op.height);
1293
+ offscreenCtx.restore();
1234
1294
  }
1235
1295
  continue;
1236
1296
  }
@@ -1616,44 +1676,72 @@ var VideoGenerator = class {
1616
1676
  tune = "animation",
1617
1677
  profile = "high",
1618
1678
  level = "4.2",
1619
- pixFmt = "yuv420p"
1679
+ pixFmt = "yuv420p",
1680
+ hasAlpha = false
1620
1681
  } = options;
1621
1682
  const totalFrames = Math.max(2, Math.round(duration * fps) + 1);
1683
+ const finalOutputPath = hasAlpha ? outputPath.replace(/\.mp4$/, ".mov") : outputPath;
1622
1684
  console.log(
1623
- `\u{1F3AC} Generating video: ${width}x${height} @ ${fps}fps, ${duration}s (${totalFrames} frames)
1624
- CRF=${crf}, preset=${preset}, tune=${tune}, profile=${profile}, level=${level}, pix_fmt=${pixFmt}`
1685
+ `\u{1F3AC} Generating video: ${width}x${height} @ ${fps}fps, ${duration}s (${totalFrames} frames)` + (hasAlpha ? " (MOV with alpha)" : "")
1625
1686
  );
1626
1687
  return new Promise(async (resolve, reject) => {
1627
- const args = [
1628
- "-y",
1629
- "-f",
1630
- "image2pipe",
1631
- "-vcodec",
1632
- "png",
1633
- "-framerate",
1634
- String(fps),
1635
- "-i",
1636
- "-",
1637
- "-c:v",
1638
- "libx264",
1639
- "-preset",
1640
- preset,
1641
- "-crf",
1642
- String(crf),
1643
- "-tune",
1644
- tune,
1645
- "-profile:v",
1646
- profile,
1647
- "-level",
1648
- level,
1649
- "-pix_fmt",
1650
- pixFmt,
1651
- "-r",
1652
- String(fps),
1653
- "-movflags",
1654
- "+faststart",
1655
- outputPath
1656
- ];
1688
+ let args;
1689
+ if (hasAlpha) {
1690
+ args = [
1691
+ "-y",
1692
+ "-f",
1693
+ "image2pipe",
1694
+ "-vcodec",
1695
+ "png",
1696
+ "-framerate",
1697
+ String(fps),
1698
+ "-i",
1699
+ "-",
1700
+ "-c:v",
1701
+ "prores_ks",
1702
+ "-profile:v",
1703
+ "4444",
1704
+ "-pix_fmt",
1705
+ "yuva444p10le",
1706
+ "-vendor",
1707
+ "apl0",
1708
+ "-r",
1709
+ String(fps),
1710
+ finalOutputPath
1711
+ ];
1712
+ console.log(` Output: ${finalOutputPath} (ProRes 4444 with alpha)`);
1713
+ } else {
1714
+ args = [
1715
+ "-y",
1716
+ "-f",
1717
+ "image2pipe",
1718
+ "-vcodec",
1719
+ "png",
1720
+ "-framerate",
1721
+ String(fps),
1722
+ "-i",
1723
+ "-",
1724
+ "-c:v",
1725
+ "libx264",
1726
+ "-preset",
1727
+ preset,
1728
+ "-crf",
1729
+ String(crf),
1730
+ "-tune",
1731
+ tune,
1732
+ "-profile:v",
1733
+ profile,
1734
+ "-level",
1735
+ level,
1736
+ "-pix_fmt",
1737
+ pixFmt,
1738
+ "-r",
1739
+ String(fps),
1740
+ "-movflags",
1741
+ "+faststart",
1742
+ outputPath
1743
+ ];
1744
+ }
1657
1745
  const ffmpeg = (0, import_child_process.spawn)(this.ffmpegPath, args, { stdio: ["pipe", "inherit", "pipe"] });
1658
1746
  let ffmpegError = "";
1659
1747
  ffmpeg.stderr.on("data", (data) => {
@@ -1902,13 +1990,21 @@ async function createTextEngine(opts = {}) {
1902
1990
  },
1903
1991
  async generateVideo(asset, options) {
1904
1992
  try {
1993
+ const hasBackground = !!asset.background?.color;
1994
+ const hasAnimation = !!asset.animation?.preset;
1995
+ const needsAlpha = !hasBackground && hasAnimation;
1996
+ console.log(
1997
+ `\u{1F3A8} Video settings: Animation=${hasAnimation}, Background=${hasBackground}, Alpha=${needsAlpha}`
1998
+ );
1905
1999
  const finalOptions = {
1906
2000
  width: asset.width ?? width,
1907
2001
  height: asset.height ?? height,
1908
2002
  fps,
1909
2003
  duration: asset.animation?.duration ?? 3,
1910
2004
  outputPath: options.outputPath ?? "output.mp4",
1911
- pixelRatio: asset.pixelRatio ?? pixelRatio
2005
+ pixelRatio: asset.pixelRatio ?? pixelRatio,
2006
+ hasAlpha: needsAlpha,
2007
+ ...options
1912
2008
  };
1913
2009
  const frameGenerator = async (time) => {
1914
2010
  return this.renderFrame(asset, time);