@shotstack/shotstack-canvas 1.0.7 → 1.0.9

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.
@@ -161,18 +161,31 @@ var RichTextAssetSchema = import_joi.default.object({
161
161
  }).unknown(false);
162
162
 
163
163
  // src/wasm/hb-loader.ts
164
+ var import_meta = {};
164
165
  var hbSingleton = null;
165
166
  async function initHB(wasmBaseURL) {
166
167
  if (hbSingleton) return hbSingleton;
167
168
  const harfbuzzjs = await import("harfbuzzjs");
168
- const hbPromise = harfbuzzjs.default;
169
- if (typeof hbPromise === "function") {
170
- hbSingleton = await hbPromise();
171
- } else if (hbPromise && typeof hbPromise.then === "function") {
172
- hbSingleton = await hbPromise;
173
- } else {
174
- hbSingleton = hbPromise;
169
+ const factory = harfbuzzjs.default;
170
+ let hbUrlFromImport;
171
+ try {
172
+ hbUrlFromImport = (await import("harfbuzzjs/hb.wasm?url")).default;
173
+ } catch {
175
174
  }
175
+ const base = (() => {
176
+ if (wasmBaseURL) return wasmBaseURL.replace(/\/$/, "");
177
+ if (hbUrlFromImport) return hbUrlFromImport.replace(/hb\.wasm(?:\?.*)?$/, "");
178
+ return new URL(".", import_meta.url).toString().replace(/\/$/, "");
179
+ })();
180
+ const locateFile = (path, scriptDir) => {
181
+ if (path.endsWith(".wasm")) {
182
+ return `${base}/${path}`.replace(/([^:])\/{2,}/g, "$1/");
183
+ }
184
+ return scriptDir ? scriptDir + path : path;
185
+ };
186
+ const initArg = { locateFile };
187
+ const maybePromise = typeof factory === "function" ? factory(initArg) : factory;
188
+ hbSingleton = maybePromise && typeof maybePromise.then === "function" ? await maybePromise : maybePromise;
176
189
  if (!hbSingleton || typeof hbSingleton.createBuffer !== "function") {
177
190
  throw new Error("Failed to initialize HarfBuzz: invalid API");
178
191
  }
@@ -186,14 +199,33 @@ var FontRegistry = class {
186
199
  fonts = /* @__PURE__ */ new Map();
187
200
  blobs = /* @__PURE__ */ new Map();
188
201
  wasmBaseURL;
202
+ initPromise;
189
203
  constructor(wasmBaseURL) {
190
204
  this.wasmBaseURL = wasmBaseURL;
191
205
  }
192
206
  async init() {
193
- if (!this.hb) this.hb = await initHB(this.wasmBaseURL);
207
+ if (this.initPromise) {
208
+ await this.initPromise;
209
+ return;
210
+ }
211
+ if (this.hb) {
212
+ return;
213
+ }
214
+ this.initPromise = this._doInit();
215
+ await this.initPromise;
216
+ }
217
+ async _doInit() {
218
+ try {
219
+ this.hb = await initHB(this.wasmBaseURL);
220
+ } catch (error) {
221
+ this.initPromise = void 0;
222
+ throw error;
223
+ }
194
224
  }
195
- getHB() {
196
- if (!this.hb) throw new Error("FontRegistry not initialized - call init() first");
225
+ async getHB() {
226
+ if (!this.hb) {
227
+ await this.init();
228
+ }
197
229
  return this.hb;
198
230
  }
199
231
  key(desc) {
@@ -212,23 +244,24 @@ var FontRegistry = class {
212
244
  this.faces.set(k, face);
213
245
  this.fonts.set(k, font);
214
246
  }
215
- getFont(desc) {
247
+ async getFont(desc) {
248
+ if (!this.hb) await this.init();
216
249
  const k = this.key(desc);
217
250
  const f = this.fonts.get(k);
218
251
  if (!f) throw new Error(`Font not registered for ${k}`);
219
252
  return f;
220
253
  }
221
- getFace(desc) {
254
+ async getFace(desc) {
255
+ if (!this.hb) await this.init();
222
256
  const k = this.key(desc);
223
257
  return this.faces.get(k);
224
258
  }
225
- /** NEW: expose units-per-em for scaling glyph paths to px */
226
- getUnitsPerEm(desc) {
227
- const face = this.getFace(desc);
259
+ async getUnitsPerEm(desc) {
260
+ const face = await this.getFace(desc);
228
261
  return face?.upem || 1e3;
229
262
  }
230
- glyphPath(desc, glyphId) {
231
- const font = this.getFont(desc);
263
+ async glyphPath(desc, glyphId) {
264
+ const font = await this.getFont(desc);
232
265
  const path = font.glyphToPath(glyphId);
233
266
  return path && path !== "" ? path : "M 0 0";
234
267
  }
@@ -239,6 +272,8 @@ var FontRegistry = class {
239
272
  this.fonts.clear();
240
273
  this.faces.clear();
241
274
  this.blobs.clear();
275
+ this.hb = void 0;
276
+ this.initPromise = void 0;
242
277
  }
243
278
  };
244
279
 
@@ -259,13 +294,13 @@ var LayoutEngine = class {
259
294
  return t;
260
295
  }
261
296
  }
262
- shapeFull(text, desc) {
263
- const hb = this.fonts.getHB();
297
+ async shapeFull(text, desc) {
298
+ const hb = await this.fonts.getHB();
264
299
  const buffer = hb.createBuffer();
265
300
  buffer.addText(text);
266
301
  buffer.guessSegmentProperties();
267
- const font = this.fonts.getFont(desc);
268
- const face = this.fonts.getFace(desc);
302
+ const font = await this.fonts.getFont(desc);
303
+ const face = await this.fonts.getFace(desc);
269
304
  const upem = face?.upem || 1e3;
270
305
  font.setScale(upem, upem);
271
306
  hb.shape(font, buffer);
@@ -273,17 +308,17 @@ var LayoutEngine = class {
273
308
  buffer.destroy();
274
309
  return result;
275
310
  }
276
- layout(params) {
311
+ async layout(params) {
277
312
  const { textTransform, desc, fontSize, letterSpacing, width } = params;
278
313
  const input = this.transformText(params.text, textTransform);
279
314
  if (!input || input.length === 0) {
280
315
  return [];
281
316
  }
282
- const shaped = this.shapeFull(input, desc);
283
- const face = this.fonts.getFace(desc);
317
+ const shaped = await this.shapeFull(input, desc);
318
+ const face = await this.fonts.getFace(desc);
284
319
  const upem = face?.upem || 1e3;
285
320
  const scale = fontSize / upem;
286
- const glyphs = shaped.map((g, i) => {
321
+ const glyphs = shaped.map((g) => {
287
322
  const charIndex = g.cl;
288
323
  let char;
289
324
  if (charIndex >= 0 && charIndex < input.length) {
@@ -387,7 +422,7 @@ function decorationGeometry(kind, p) {
387
422
  }
388
423
 
389
424
  // src/core/drawops.ts
390
- function buildDrawOps(p) {
425
+ async function buildDrawOps(p) {
391
426
  const ops = [];
392
427
  ops.push({
393
428
  op: "BeginFrame",
@@ -398,7 +433,7 @@ function buildDrawOps(p) {
398
433
  bg: p.background ? { color: p.background.color, opacity: p.background.opacity, radius: p.background.borderRadius } : void 0
399
434
  });
400
435
  if (p.lines.length === 0) return ops;
401
- const upem = Math.max(1, p.getUnitsPerEm?.() ?? 1e3);
436
+ const upem = Math.max(1, await p.getUnitsPerEm());
402
437
  const scale = p.font.size / upem;
403
438
  const blockHeight = p.lines[p.lines.length - 1].y;
404
439
  let blockY;
@@ -434,7 +469,7 @@ function buildDrawOps(p) {
434
469
  let xCursor = lineX;
435
470
  const baselineY = blockY + line.y - p.font.size;
436
471
  for (const glyph of line.glyphs) {
437
- const path = p.glyphPathProvider(glyph.id);
472
+ const path = await p.glyphPathProvider(glyph.id);
438
473
  if (!path || path === "M 0 0") {
439
474
  xCursor += glyph.xAdvance;
440
475
  continue;
@@ -1426,6 +1461,7 @@ async function createTextEngine(opts = {}) {
1426
1461
  const fonts = new FontRegistry(wasmBaseURL);
1427
1462
  const layout = new LayoutEngine(fonts);
1428
1463
  const videoGenerator = new VideoGenerator();
1464
+ await fonts.init();
1429
1465
  async function ensureFonts(asset) {
1430
1466
  if (asset.customFonts) {
1431
1467
  for (const cf of asset.customFonts) {
@@ -1467,7 +1503,7 @@ async function createTextEngine(opts = {}) {
1467
1503
  async renderFrame(asset, tSeconds) {
1468
1504
  const main = await ensureFonts(asset);
1469
1505
  const desc = { family: main.family, weight: `${main.weight}`, style: main.style };
1470
- const lines = layout.layout({
1506
+ const lines = await layout.layout({
1471
1507
  text: asset.text,
1472
1508
  width: asset.width ?? width,
1473
1509
  letterSpacing: asset.style?.letterSpacing ?? 0,
@@ -1480,7 +1516,7 @@ async function createTextEngine(opts = {}) {
1480
1516
  const canvasW = asset.width ?? width;
1481
1517
  const canvasH = asset.height ?? height;
1482
1518
  const canvasPR = asset.pixelRatio ?? pixelRatio;
1483
- const ops0 = buildDrawOps({
1519
+ const ops0 = await buildDrawOps({
1484
1520
  canvas: { width: canvasW, height: canvasH, pixelRatio: canvasPR },
1485
1521
  textRect,
1486
1522
  lines,
@@ -1502,7 +1538,6 @@ async function createTextEngine(opts = {}) {
1502
1538
  align: asset.align ?? { horizontal: "left", vertical: "middle" },
1503
1539
  background: asset.background,
1504
1540
  glyphPathProvider: (gid) => fonts.glyphPath(desc, gid),
1505
- /** NEW: provide UPEM so drawops can compute scale */
1506
1541
  getUnitsPerEm: () => fonts.getUnitsPerEm(desc)
1507
1542
  });
1508
1543
  const ops = applyAnimation(ops0, lines, {
@@ -1538,6 +1573,9 @@ async function createTextEngine(opts = {}) {
1538
1573
  return this.renderFrame(asset, time);
1539
1574
  };
1540
1575
  await videoGenerator.generateVideo(frameGenerator, finalOptions);
1576
+ },
1577
+ destroy() {
1578
+ fonts.destroy();
1541
1579
  }
1542
1580
  };
1543
1581
  }