@shotstack/shotstack-canvas 1.0.9 → 1.1.1

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.
@@ -30,7 +30,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/env/entry.node.ts
31
31
  var entry_node_exports = {};
32
32
  __export(entry_node_exports, {
33
- createTextEngine: () => createTextEngine
33
+ createTextEngine: () => createTextEngine,
34
+ isGlyphFill: () => isGlyphFill2,
35
+ isShadowFill: () => isShadowFill2
34
36
  });
35
37
  module.exports = __toCommonJS(entry_node_exports);
36
38
 
@@ -161,35 +163,31 @@ var RichTextAssetSchema = import_joi.default.object({
161
163
  }).unknown(false);
162
164
 
163
165
  // src/wasm/hb-loader.ts
164
- var import_meta = {};
165
166
  var hbSingleton = null;
166
167
  async function initHB(wasmBaseURL) {
167
168
  if (hbSingleton) return hbSingleton;
168
- const harfbuzzjs = await import("harfbuzzjs");
169
- const factory = harfbuzzjs.default;
170
- let hbUrlFromImport;
171
169
  try {
172
- hbUrlFromImport = (await import("harfbuzzjs/hb.wasm?url")).default;
173
- } catch {
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/");
170
+ const mod = await import("harfbuzzjs");
171
+ const candidate = mod.default;
172
+ let hb;
173
+ if (typeof candidate === "function") {
174
+ hb = await candidate();
175
+ } else if (candidate && typeof candidate.then === "function") {
176
+ hb = await candidate;
177
+ } else {
178
+ hb = candidate;
183
179
  }
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;
189
- if (!hbSingleton || typeof hbSingleton.createBuffer !== "function") {
190
- throw new Error("Failed to initialize HarfBuzz: invalid API");
180
+ if (!hb || typeof hb.createBuffer !== "function" || typeof hb.createFont !== "function") {
181
+ throw new Error("Failed to initialize HarfBuzz: unexpected export shape from 'harfbuzzjs'.");
182
+ }
183
+ void wasmBaseURL;
184
+ hbSingleton = hb;
185
+ return hbSingleton;
186
+ } catch (err) {
187
+ throw new Error(
188
+ `Failed to initialize HarfBuzz: ${err instanceof Error ? err.message : String(err)}`
189
+ );
191
190
  }
192
- return hbSingleton;
193
191
  }
194
192
 
195
193
  // src/core/font-registry.ts
@@ -212,7 +210,13 @@ var FontRegistry = class {
212
210
  return;
213
211
  }
214
212
  this.initPromise = this._doInit();
215
- await this.initPromise;
213
+ try {
214
+ await this.initPromise;
215
+ } catch (err) {
216
+ throw new Error(
217
+ `Failed to initialize FontRegistry: ${err instanceof Error ? err.message : String(err)}`
218
+ );
219
+ }
216
220
  }
217
221
  async _doInit() {
218
222
  try {
@@ -224,7 +228,13 @@ var FontRegistry = class {
224
228
  }
225
229
  async getHB() {
226
230
  if (!this.hb) {
227
- await this.init();
231
+ try {
232
+ await this.init();
233
+ } catch (err) {
234
+ throw new Error(
235
+ `Failed to get HarfBuzz instance: ${err instanceof Error ? err.message : String(err)}`
236
+ );
237
+ }
228
238
  }
229
239
  return this.hb;
230
240
  }
@@ -232,48 +242,111 @@ var FontRegistry = class {
232
242
  return `${desc.family}__${desc.weight ?? "400"}__${desc.style ?? "normal"}`;
233
243
  }
234
244
  async registerFromBytes(bytes, desc) {
235
- if (!this.hb) await this.init();
236
- const k = this.key(desc);
237
- if (this.fonts.has(k)) return;
238
- const blob = this.hb.createBlob(bytes);
239
- const face = this.hb.createFace(blob, 0);
240
- const font = this.hb.createFont(face);
241
- const upem = face.upem || 1e3;
242
- font.setScale(upem, upem);
243
- this.blobs.set(k, blob);
244
- this.faces.set(k, face);
245
- this.fonts.set(k, font);
245
+ try {
246
+ if (!this.hb) await this.init();
247
+ const k = this.key(desc);
248
+ if (this.fonts.has(k)) return;
249
+ const blob = this.hb.createBlob(bytes);
250
+ const face = this.hb.createFace(blob, 0);
251
+ const font = this.hb.createFont(face);
252
+ const upem = face.upem || 1e3;
253
+ font.setScale(upem, upem);
254
+ this.blobs.set(k, blob);
255
+ this.faces.set(k, face);
256
+ this.fonts.set(k, font);
257
+ } catch (err) {
258
+ throw new Error(
259
+ `Failed to register font "${desc.family}": ${err instanceof Error ? err.message : String(err)}`
260
+ );
261
+ }
246
262
  }
247
263
  async getFont(desc) {
248
- if (!this.hb) await this.init();
249
- const k = this.key(desc);
250
- const f = this.fonts.get(k);
251
- if (!f) throw new Error(`Font not registered for ${k}`);
252
- return f;
264
+ try {
265
+ if (!this.hb) await this.init();
266
+ const k = this.key(desc);
267
+ const f = this.fonts.get(k);
268
+ if (!f) throw new Error(`Font not registered for ${k}`);
269
+ return f;
270
+ } catch (err) {
271
+ if (err instanceof Error && err.message.includes("Font not registered")) {
272
+ throw err;
273
+ }
274
+ throw new Error(
275
+ `Failed to get font "${desc.family}": ${err instanceof Error ? err.message : String(err)}`
276
+ );
277
+ }
253
278
  }
254
279
  async getFace(desc) {
255
- if (!this.hb) await this.init();
256
- const k = this.key(desc);
257
- return this.faces.get(k);
280
+ try {
281
+ if (!this.hb) await this.init();
282
+ const k = this.key(desc);
283
+ return this.faces.get(k);
284
+ } catch (err) {
285
+ throw new Error(
286
+ `Failed to get face for "${desc.family}": ${err instanceof Error ? err.message : String(err)}`
287
+ );
288
+ }
258
289
  }
259
290
  async getUnitsPerEm(desc) {
260
- const face = await this.getFace(desc);
261
- return face?.upem || 1e3;
291
+ try {
292
+ const face = await this.getFace(desc);
293
+ return face?.upem || 1e3;
294
+ } catch (err) {
295
+ throw new Error(
296
+ `Failed to get units per em for "${desc.family}": ${err instanceof Error ? err.message : String(err)}`
297
+ );
298
+ }
262
299
  }
263
300
  async glyphPath(desc, glyphId) {
264
- const font = await this.getFont(desc);
265
- const path = font.glyphToPath(glyphId);
266
- return path && path !== "" ? path : "M 0 0";
301
+ try {
302
+ const font = await this.getFont(desc);
303
+ const path = font.glyphToPath(glyphId);
304
+ return path && path !== "" ? path : "M 0 0";
305
+ } catch (err) {
306
+ throw new Error(
307
+ `Failed to get glyph path for glyph ${glyphId} in font "${desc.family}": ${err instanceof Error ? err.message : String(err)}`
308
+ );
309
+ }
267
310
  }
268
311
  destroy() {
269
- for (const [, f] of this.fonts) f.destroy?.();
270
- for (const [, f] of this.faces) f.destroy?.();
271
- for (const [, b] of this.blobs) b.destroy?.();
272
- this.fonts.clear();
273
- this.faces.clear();
274
- this.blobs.clear();
275
- this.hb = void 0;
276
- this.initPromise = void 0;
312
+ try {
313
+ for (const [, f] of this.fonts) {
314
+ try {
315
+ f.destroy();
316
+ } catch (err) {
317
+ console.error(
318
+ `Error destroying font: ${err instanceof Error ? err.message : String(err)}`
319
+ );
320
+ }
321
+ }
322
+ for (const [, f] of this.faces) {
323
+ try {
324
+ f.destroy();
325
+ } catch (err) {
326
+ console.error(
327
+ `Error destroying face: ${err instanceof Error ? err.message : String(err)}`
328
+ );
329
+ }
330
+ }
331
+ for (const [, b] of this.blobs) {
332
+ try {
333
+ b.destroy();
334
+ } catch (err) {
335
+ console.error(
336
+ `Error destroying blob: ${err instanceof Error ? err.message : String(err)}`
337
+ );
338
+ }
339
+ }
340
+ this.fonts.clear();
341
+ this.faces.clear();
342
+ this.blobs.clear();
343
+ this.hb = void 0;
344
+ this.initPromise = void 0;
345
+ } catch (err) {
346
+ console.error(
347
+ `Error during FontRegistry cleanup: ${err instanceof Error ? err.message : String(err)}`
348
+ );
349
+ }
277
350
  }
278
351
  };
279
352
 
@@ -295,112 +368,145 @@ var LayoutEngine = class {
295
368
  }
296
369
  }
297
370
  async shapeFull(text, desc) {
298
- const hb = await this.fonts.getHB();
299
- const buffer = hb.createBuffer();
300
- buffer.addText(text);
301
- buffer.guessSegmentProperties();
302
- const font = await this.fonts.getFont(desc);
303
- const face = await this.fonts.getFace(desc);
304
- const upem = face?.upem || 1e3;
305
- font.setScale(upem, upem);
306
- hb.shape(font, buffer);
307
- const result = buffer.json();
308
- buffer.destroy();
309
- return result;
371
+ try {
372
+ const hb = await this.fonts.getHB();
373
+ const buffer = hb.createBuffer();
374
+ try {
375
+ buffer.addText(text);
376
+ buffer.guessSegmentProperties();
377
+ const font = await this.fonts.getFont(desc);
378
+ const face = await this.fonts.getFace(desc);
379
+ const upem = face?.upem || 1e3;
380
+ font.setScale(upem, upem);
381
+ hb.shape(font, buffer);
382
+ const result = buffer.json();
383
+ return result;
384
+ } finally {
385
+ try {
386
+ buffer.destroy();
387
+ } catch (err) {
388
+ console.error(
389
+ `Error destroying HarfBuzz buffer: ${err instanceof Error ? err.message : String(err)}`
390
+ );
391
+ }
392
+ }
393
+ } catch (err) {
394
+ throw new Error(
395
+ `Failed to shape text with font "${desc.family}": ${err instanceof Error ? err.message : String(err)}`
396
+ );
397
+ }
310
398
  }
311
399
  async layout(params) {
312
- const { textTransform, desc, fontSize, letterSpacing, width } = params;
313
- const input = this.transformText(params.text, textTransform);
314
- if (!input || input.length === 0) {
315
- return [];
316
- }
317
- const shaped = await this.shapeFull(input, desc);
318
- const face = await this.fonts.getFace(desc);
319
- const upem = face?.upem || 1e3;
320
- const scale = fontSize / upem;
321
- const glyphs = shaped.map((g) => {
322
- const charIndex = g.cl;
323
- let char;
324
- if (charIndex >= 0 && charIndex < input.length) {
325
- char = input[charIndex];
400
+ try {
401
+ const { textTransform, desc, fontSize, letterSpacing, width } = params;
402
+ const input = this.transformText(params.text, textTransform);
403
+ if (!input || input.length === 0) {
404
+ return [];
326
405
  }
327
- return {
328
- id: g.g,
329
- xAdvance: g.ax * scale + letterSpacing,
330
- xOffset: g.dx * scale,
331
- yOffset: -g.dy * scale,
332
- cluster: g.cl,
333
- char
334
- // This now correctly maps to the original character
335
- };
336
- });
337
- const lines = [];
338
- let currentLine = [];
339
- let currentWidth = 0;
340
- const spaceIndices = /* @__PURE__ */ new Set();
341
- for (let i = 0; i < input.length; i++) {
342
- if (input[i] === " ") {
343
- spaceIndices.add(i);
406
+ let shaped;
407
+ try {
408
+ shaped = await this.shapeFull(input, desc);
409
+ } catch (err) {
410
+ throw new Error(`Text shaping failed: ${err instanceof Error ? err.message : String(err)}`);
344
411
  }
345
- }
346
- let lastBreakIndex = -1;
347
- for (let i = 0; i < glyphs.length; i++) {
348
- const glyph = glyphs[i];
349
- const glyphWidth = glyph.xAdvance;
350
- if (glyph.char === "\n") {
351
- if (currentLine.length > 0) {
352
- lines.push({
353
- glyphs: currentLine,
354
- width: currentWidth,
355
- y: 0
356
- });
412
+ let upem;
413
+ try {
414
+ const face = await this.fonts.getFace(desc);
415
+ upem = face?.upem || 1e3;
416
+ } catch (err) {
417
+ throw new Error(
418
+ `Failed to get font metrics: ${err instanceof Error ? err.message : String(err)}`
419
+ );
420
+ }
421
+ const scale = fontSize / upem;
422
+ const glyphs = shaped.map((g) => {
423
+ const charIndex = g.cl;
424
+ let char;
425
+ if (charIndex >= 0 && charIndex < input.length) {
426
+ char = input[charIndex];
427
+ }
428
+ return {
429
+ id: g.g,
430
+ xAdvance: g.ax * scale + letterSpacing,
431
+ xOffset: g.dx * scale,
432
+ yOffset: -g.dy * scale,
433
+ cluster: g.cl,
434
+ char
435
+ };
436
+ });
437
+ const lines = [];
438
+ let currentLine = [];
439
+ let currentWidth = 0;
440
+ const spaceIndices = /* @__PURE__ */ new Set();
441
+ for (let i = 0; i < input.length; i++) {
442
+ if (input[i] === " ") {
443
+ spaceIndices.add(i);
357
444
  }
358
- currentLine = [];
359
- currentWidth = 0;
360
- lastBreakIndex = i;
361
- continue;
362
445
  }
363
- if (currentWidth + glyphWidth > width && currentLine.length > 0) {
364
- if (lastBreakIndex > -1) {
365
- const breakPoint = lastBreakIndex - (i - currentLine.length) + 1;
366
- const nextLine = currentLine.splice(breakPoint);
367
- const lineWidth = currentLine.reduce((sum, g) => sum + g.xAdvance, 0);
368
- lines.push({
369
- glyphs: currentLine,
370
- width: lineWidth,
371
- y: 0
372
- });
373
- currentLine = nextLine;
374
- currentWidth = nextLine.reduce((sum, g) => sum + g.xAdvance, 0);
375
- } else {
376
- lines.push({
377
- glyphs: currentLine,
378
- width: currentWidth,
379
- y: 0
380
- });
446
+ let lastBreakIndex = -1;
447
+ for (let i = 0; i < glyphs.length; i++) {
448
+ const glyph = glyphs[i];
449
+ const glyphWidth = glyph.xAdvance;
450
+ if (glyph.char === "\n") {
451
+ if (currentLine.length > 0) {
452
+ lines.push({
453
+ glyphs: currentLine,
454
+ width: currentWidth,
455
+ y: 0
456
+ });
457
+ }
381
458
  currentLine = [];
382
459
  currentWidth = 0;
460
+ lastBreakIndex = i;
461
+ continue;
462
+ }
463
+ if (currentWidth + glyphWidth > width && currentLine.length > 0) {
464
+ if (lastBreakIndex > -1) {
465
+ const breakPoint = lastBreakIndex - (i - currentLine.length) + 1;
466
+ const nextLine = currentLine.splice(breakPoint);
467
+ const lineWidth = currentLine.reduce((sum, g) => sum + g.xAdvance, 0);
468
+ lines.push({
469
+ glyphs: currentLine,
470
+ width: lineWidth,
471
+ y: 0
472
+ });
473
+ currentLine = nextLine;
474
+ currentWidth = nextLine.reduce((sum, g) => sum + g.xAdvance, 0);
475
+ } else {
476
+ lines.push({
477
+ glyphs: currentLine,
478
+ width: currentWidth,
479
+ y: 0
480
+ });
481
+ currentLine = [];
482
+ currentWidth = 0;
483
+ }
484
+ lastBreakIndex = -1;
485
+ }
486
+ currentLine.push(glyph);
487
+ currentWidth += glyphWidth;
488
+ if (spaceIndices.has(glyph.cluster)) {
489
+ lastBreakIndex = i;
383
490
  }
384
- lastBreakIndex = -1;
385
491
  }
386
- currentLine.push(glyph);
387
- currentWidth += glyphWidth;
388
- if (spaceIndices.has(glyph.cluster)) {
389
- lastBreakIndex = i;
492
+ if (currentLine.length > 0) {
493
+ lines.push({
494
+ glyphs: currentLine,
495
+ width: currentWidth,
496
+ y: 0
497
+ });
390
498
  }
499
+ const lineHeight = params.lineHeight * fontSize;
500
+ for (let i = 0; i < lines.length; i++) {
501
+ lines[i].y = (i + 1) * lineHeight;
502
+ }
503
+ return lines;
504
+ } catch (err) {
505
+ if (err instanceof Error) {
506
+ throw err;
507
+ }
508
+ throw new Error(`Layout failed: ${String(err)}`);
391
509
  }
392
- if (currentLine.length > 0) {
393
- lines.push({
394
- glyphs: currentLine,
395
- width: currentWidth,
396
- y: 0
397
- });
398
- }
399
- const lineHeight = params.lineHeight * fontSize;
400
- for (let i = 0; i < lines.length; i++) {
401
- lines[i].y = (i + 1) * lineHeight;
402
- }
403
- return lines;
404
510
  }
405
511
  };
406
512
 
@@ -492,7 +598,6 @@ async function buildDrawOps(p) {
492
598
  path,
493
599
  x: glyphX + p.shadow.offsetX,
494
600
  y: glyphY + p.shadow.offsetY,
495
- // @ts-ignore scale propagated to painters
496
601
  scale,
497
602
  fill: { kind: "solid", color: p.shadow.color, opacity: p.shadow.opacity }
498
603
  });
@@ -503,7 +608,6 @@ async function buildDrawOps(p) {
503
608
  path,
504
609
  x: glyphX,
505
610
  y: glyphY,
506
- // @ts-ignore scale propagated to painters
507
611
  scale,
508
612
  width: p.stroke.width,
509
613
  color: p.stroke.color,
@@ -515,7 +619,6 @@ async function buildDrawOps(p) {
515
619
  path,
516
620
  x: glyphX,
517
621
  y: glyphY,
518
- // @ts-ignore scale propagated to painters
519
622
  scale,
520
623
  fill
521
624
  });
@@ -1091,30 +1194,32 @@ async function createNodePainter(opts) {
1091
1194
  continue;
1092
1195
  }
1093
1196
  if (op.op === "FillPath") {
1197
+ const fillOp = op;
1094
1198
  ctx.save();
1095
- ctx.translate(op.x, op.y);
1096
- const s = op.scale ?? 1;
1199
+ ctx.translate(fillOp.x, fillOp.y);
1200
+ const s = fillOp.scale ?? 1;
1097
1201
  ctx.scale(s, -s);
1098
1202
  ctx.beginPath();
1099
- drawSvgPathOnCtx(ctx, op.path);
1100
- const bbox = op.gradientBBox ?? globalBox;
1101
- const fill = makeGradientFromBBox(ctx, op.fill, bbox);
1203
+ drawSvgPathOnCtx(ctx, fillOp.path);
1204
+ const bbox = fillOp.gradientBBox ?? globalBox;
1205
+ const fill = makeGradientFromBBox(ctx, fillOp.fill, bbox);
1102
1206
  ctx.fillStyle = fill;
1103
1207
  ctx.fill();
1104
1208
  ctx.restore();
1105
1209
  continue;
1106
1210
  }
1107
1211
  if (op.op === "StrokePath") {
1212
+ const strokeOp = op;
1108
1213
  ctx.save();
1109
- ctx.translate(op.x, op.y);
1110
- const s = op.scale ?? 1;
1214
+ ctx.translate(strokeOp.x, strokeOp.y);
1215
+ const s = strokeOp.scale ?? 1;
1111
1216
  ctx.scale(s, -s);
1112
1217
  const invAbs = 1 / Math.abs(s);
1113
1218
  ctx.beginPath();
1114
- drawSvgPathOnCtx(ctx, op.path);
1115
- const c = parseHex6(op.color, op.opacity);
1219
+ drawSvgPathOnCtx(ctx, strokeOp.path);
1220
+ const c = parseHex6(strokeOp.color, strokeOp.opacity);
1116
1221
  ctx.strokeStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
1117
- ctx.lineWidth = op.width * invAbs;
1222
+ ctx.lineWidth = strokeOp.width * invAbs;
1118
1223
  ctx.lineJoin = "round";
1119
1224
  ctx.lineCap = "round";
1120
1225
  ctx.stroke();
@@ -1146,17 +1251,20 @@ function makeGradientFromBBox(ctx, spec, box) {
1146
1251
  const c = parseHex6(spec.color, spec.opacity);
1147
1252
  return `rgba(${c.r},${c.g},${c.b},${c.a})`;
1148
1253
  }
1149
- const cx = box.x + box.w / 2, cy = box.y + box.h / 2, r = Math.max(box.w, box.h) / 2;
1254
+ const cx = box.x + box.w / 2;
1255
+ const cy = box.y + box.h / 2;
1256
+ const r = Math.max(box.w, box.h) / 2;
1150
1257
  const addStops = (g) => {
1151
- const op = spec.opacity ?? 1;
1152
- for (const s of spec.stops) {
1153
- const c = parseHex6(s.color, op);
1258
+ const opacity = spec.kind === "linear" || spec.kind === "radial" ? spec.opacity : 1;
1259
+ const stops = spec.kind === "linear" || spec.kind === "radial" ? spec.stops : [];
1260
+ for (const s of stops) {
1261
+ const c = parseHex6(s.color, opacity);
1154
1262
  g.addColorStop(s.offset, `rgba(${c.r},${c.g},${c.b},${c.a})`);
1155
1263
  }
1156
1264
  return g;
1157
1265
  };
1158
1266
  if (spec.kind === "linear") {
1159
- const rad = (spec.angle || 0) * Math.PI / 180;
1267
+ const rad = spec.angle * Math.PI / 180;
1160
1268
  const x1 = cx + Math.cos(rad + Math.PI) * r;
1161
1269
  const y1 = cy + Math.sin(rad + Math.PI) * r;
1162
1270
  const x2 = cx + Math.cos(rad) * r;
@@ -1167,22 +1275,34 @@ function makeGradientFromBBox(ctx, spec, box) {
1167
1275
  }
1168
1276
  }
1169
1277
  function computeGlobalTextBounds(ops) {
1170
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1278
+ let minX = Infinity;
1279
+ let minY = Infinity;
1280
+ let maxX = -Infinity;
1281
+ let maxY = -Infinity;
1171
1282
  for (const op of ops) {
1172
- if (op.op !== "FillPath" || op.isShadow) continue;
1173
- const b = computePathBounds2(op.path);
1174
- const s = op.scale ?? 1;
1175
- const x1 = op.x + s * b.x;
1176
- const x2 = op.x + s * (b.x + b.w);
1177
- const y1 = op.y - s * (b.y + b.h);
1178
- const y2 = op.y - s * b.y;
1283
+ if (op.op !== "FillPath") continue;
1284
+ const fillOp = op;
1285
+ if (fillOp.isShadow) continue;
1286
+ const b = computePathBounds2(fillOp.path);
1287
+ const s = fillOp.scale ?? 1;
1288
+ const x1 = fillOp.x + s * b.x;
1289
+ const x2 = fillOp.x + s * (b.x + b.w);
1290
+ const y1 = fillOp.y - s * (b.y + b.h);
1291
+ const y2 = fillOp.y - s * b.y;
1179
1292
  if (x1 < minX) minX = x1;
1180
1293
  if (y1 < minY) minY = y1;
1181
1294
  if (x2 > maxX) maxX = x2;
1182
1295
  if (y2 > maxY) maxY = y2;
1183
1296
  }
1184
- if (minX === Infinity) return { x: 0, y: 0, w: 1, h: 1 };
1185
- return { x: minX, y: minY, w: Math.max(1, maxX - minX), h: Math.max(1, maxY - minY) };
1297
+ if (minX === Infinity) {
1298
+ return { x: 0, y: 0, w: 1, h: 1 };
1299
+ }
1300
+ return {
1301
+ x: minX,
1302
+ y: minY,
1303
+ w: Math.max(1, maxX - minX),
1304
+ h: Math.max(1, maxY - minY)
1305
+ };
1186
1306
  }
1187
1307
  function drawSvgPathOnCtx(ctx, d) {
1188
1308
  const t = tokenizePath2(d);
@@ -1224,13 +1344,18 @@ function drawSvgPathOnCtx(ctx, d) {
1224
1344
  ctx.closePath();
1225
1345
  break;
1226
1346
  }
1347
+ default:
1348
+ break;
1227
1349
  }
1228
1350
  }
1229
1351
  }
1230
1352
  function computePathBounds2(d) {
1231
1353
  const t = tokenizePath2(d);
1232
1354
  let i = 0;
1233
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1355
+ let minX = Infinity;
1356
+ let minY = Infinity;
1357
+ let maxX = -Infinity;
1358
+ let maxY = -Infinity;
1234
1359
  const touch = (x, y) => {
1235
1360
  if (x < minX) minX = x;
1236
1361
  if (y < minY) minY = y;
@@ -1270,10 +1395,19 @@ function computePathBounds2(d) {
1270
1395
  }
1271
1396
  case "Z":
1272
1397
  break;
1398
+ default:
1399
+ break;
1273
1400
  }
1274
1401
  }
1275
- if (minX === Infinity) return { x: 0, y: 0, w: 0, h: 0 };
1276
- return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
1402
+ if (minX === Infinity) {
1403
+ return { x: 0, y: 0, w: 0, h: 0 };
1404
+ }
1405
+ return {
1406
+ x: minX,
1407
+ y: minY,
1408
+ w: maxX - minX,
1409
+ h: maxY - minY
1410
+ };
1277
1411
  }
1278
1412
  function roundRectPath(ctx, x, y, w, h, r) {
1279
1413
  ctx.moveTo(x + r, y);
@@ -1302,20 +1436,54 @@ function bufferToArrayBuffer(buf) {
1302
1436
  return ab.slice(byteOffset, byteOffset + byteLength);
1303
1437
  }
1304
1438
  async function loadFileOrHttpToArrayBuffer(pathOrUrl) {
1305
- if (/^https?:\/\//.test(pathOrUrl)) {
1306
- const client = pathOrUrl.startsWith("https:") ? https : http;
1307
- const buf2 = await new Promise((resolve, reject) => {
1308
- client.get(pathOrUrl, (res) => {
1309
- const chunks = [];
1310
- res.on("data", (d) => chunks.push(d));
1311
- res.on("end", () => resolve(Buffer.concat(chunks)));
1312
- res.on("error", reject);
1313
- }).on("error", reject);
1314
- });
1315
- return bufferToArrayBuffer(buf2);
1439
+ try {
1440
+ if (/^https?:\/\//.test(pathOrUrl)) {
1441
+ const client = pathOrUrl.startsWith("https:") ? https : http;
1442
+ const buf2 = await new Promise((resolve, reject) => {
1443
+ const request = client.get(pathOrUrl, (res) => {
1444
+ const { statusCode } = res;
1445
+ if (statusCode && (statusCode < 200 || statusCode >= 300)) {
1446
+ reject(new Error(`HTTP request failed with status ${statusCode} for ${pathOrUrl}`));
1447
+ res.resume();
1448
+ return;
1449
+ }
1450
+ const chunks = [];
1451
+ res.on("data", (chunk) => {
1452
+ chunks.push(chunk);
1453
+ });
1454
+ res.on("end", () => {
1455
+ try {
1456
+ resolve(Buffer.concat(chunks));
1457
+ } catch (err) {
1458
+ reject(
1459
+ new Error(
1460
+ `Failed to concatenate response chunks: ${err instanceof Error ? err.message : String(err)}`
1461
+ )
1462
+ );
1463
+ }
1464
+ });
1465
+ res.on("error", (err) => {
1466
+ reject(new Error(`Response error for ${pathOrUrl}: ${err.message}`));
1467
+ });
1468
+ });
1469
+ request.on("error", (err) => {
1470
+ reject(new Error(`Request error for ${pathOrUrl}: ${err.message}`));
1471
+ });
1472
+ request.setTimeout(3e4, () => {
1473
+ request.destroy();
1474
+ reject(new Error(`Request timeout after 30s for ${pathOrUrl}`));
1475
+ });
1476
+ });
1477
+ return bufferToArrayBuffer(buf2);
1478
+ }
1479
+ const buf = await (0, import_promises.readFile)(pathOrUrl);
1480
+ return bufferToArrayBuffer(buf);
1481
+ } catch (err) {
1482
+ if (err instanceof Error) {
1483
+ throw new Error(`Failed to load ${pathOrUrl}: ${err.message}`);
1484
+ }
1485
+ throw new Error(`Failed to load ${pathOrUrl}: ${String(err)}`);
1316
1486
  }
1317
- const buf = await (0, import_promises.readFile)(pathOrUrl);
1318
- return bufferToArrayBuffer(buf);
1319
1487
  }
1320
1488
 
1321
1489
  // src/core/video-generator.ts
@@ -1451,6 +1619,14 @@ var VideoGenerator = class {
1451
1619
  }
1452
1620
  };
1453
1621
 
1622
+ // src/types.ts
1623
+ var isShadowFill2 = (op) => {
1624
+ return op.op === "FillPath" && op.isShadow === true;
1625
+ };
1626
+ var isGlyphFill2 = (op) => {
1627
+ return op.op === "FillPath" && op.isShadow !== true;
1628
+ };
1629
+
1454
1630
  // src/env/entry.node.ts
1455
1631
  async function createTextEngine(opts = {}) {
1456
1632
  const width = opts.width ?? CANVAS_CONFIG.DEFAULTS.width;
@@ -1461,126 +1637,214 @@ async function createTextEngine(opts = {}) {
1461
1637
  const fonts = new FontRegistry(wasmBaseURL);
1462
1638
  const layout = new LayoutEngine(fonts);
1463
1639
  const videoGenerator = new VideoGenerator();
1464
- await fonts.init();
1640
+ try {
1641
+ await fonts.init();
1642
+ } catch (err) {
1643
+ throw new Error(
1644
+ `Failed to initialize font registry: ${err instanceof Error ? err.message : String(err)}`
1645
+ );
1646
+ }
1465
1647
  async function ensureFonts(asset) {
1466
- if (asset.customFonts) {
1467
- for (const cf of asset.customFonts) {
1468
- const bytes = await loadFileOrHttpToArrayBuffer(cf.src);
1469
- await fonts.registerFromBytes(bytes, {
1470
- family: cf.family,
1471
- weight: cf.weight ?? "400",
1472
- style: cf.style ?? "normal"
1473
- });
1648
+ try {
1649
+ if (asset.customFonts) {
1650
+ for (const cf of asset.customFonts) {
1651
+ try {
1652
+ const bytes = await loadFileOrHttpToArrayBuffer(cf.src);
1653
+ await fonts.registerFromBytes(bytes, {
1654
+ family: cf.family,
1655
+ weight: cf.weight ?? "400",
1656
+ style: cf.style ?? "normal"
1657
+ });
1658
+ } catch (err) {
1659
+ throw new Error(
1660
+ `Failed to load custom font "${cf.family}" from ${cf.src}: ${err instanceof Error ? err.message : String(err)}`
1661
+ );
1662
+ }
1663
+ }
1474
1664
  }
1665
+ const main = asset.font ?? {
1666
+ family: "Roboto",
1667
+ weight: "400",
1668
+ style: "normal",
1669
+ size: 48,
1670
+ color: "#000000",
1671
+ opacity: 1
1672
+ };
1673
+ return main;
1674
+ } catch (err) {
1675
+ if (err instanceof Error) {
1676
+ throw err;
1677
+ }
1678
+ throw new Error(`Failed to ensure fonts: ${String(err)}`);
1475
1679
  }
1476
- const main = asset.font ?? {
1477
- family: "Roboto",
1478
- weight: "400",
1479
- style: "normal",
1480
- size: 48,
1481
- color: "#000000",
1482
- opacity: 1
1483
- };
1484
- return main;
1485
1680
  }
1486
1681
  return {
1487
1682
  validate(input) {
1488
- const { value, error } = RichTextAssetSchema.validate(input, {
1489
- abortEarly: false,
1490
- convert: true
1491
- });
1492
- if (error) throw error;
1493
- return { value };
1683
+ try {
1684
+ const { value, error } = RichTextAssetSchema.validate(input, {
1685
+ abortEarly: false,
1686
+ convert: true
1687
+ });
1688
+ if (error) throw error;
1689
+ return { value };
1690
+ } catch (err) {
1691
+ if (err instanceof Error) {
1692
+ throw new Error(`Validation failed: ${err.message}`);
1693
+ }
1694
+ throw new Error(`Validation failed: ${String(err)}`);
1695
+ }
1494
1696
  },
1495
1697
  async registerFontFromFile(path, desc) {
1496
- const bytes = await loadFileOrHttpToArrayBuffer(path);
1497
- await fonts.registerFromBytes(bytes, desc);
1698
+ try {
1699
+ const bytes = await loadFileOrHttpToArrayBuffer(path);
1700
+ await fonts.registerFromBytes(bytes, desc);
1701
+ } catch (err) {
1702
+ throw new Error(
1703
+ `Failed to register font "${desc.family}" from file ${path}: ${err instanceof Error ? err.message : String(err)}`
1704
+ );
1705
+ }
1498
1706
  },
1499
1707
  async registerFontFromUrl(url, desc) {
1500
- const bytes = await loadFileOrHttpToArrayBuffer(url);
1501
- await fonts.registerFromBytes(bytes, desc);
1708
+ try {
1709
+ const bytes = await loadFileOrHttpToArrayBuffer(url);
1710
+ await fonts.registerFromBytes(bytes, desc);
1711
+ } catch (err) {
1712
+ throw new Error(
1713
+ `Failed to register font "${desc.family}" from URL ${url}: ${err instanceof Error ? err.message : String(err)}`
1714
+ );
1715
+ }
1502
1716
  },
1503
1717
  async renderFrame(asset, tSeconds) {
1504
- const main = await ensureFonts(asset);
1505
- const desc = { family: main.family, weight: `${main.weight}`, style: main.style };
1506
- const lines = await layout.layout({
1507
- text: asset.text,
1508
- width: asset.width ?? width,
1509
- letterSpacing: asset.style?.letterSpacing ?? 0,
1510
- fontSize: main.size,
1511
- lineHeight: asset.style?.lineHeight ?? 1.2,
1512
- desc,
1513
- textTransform: asset.style?.textTransform ?? "none"
1514
- });
1515
- const textRect = { x: 0, y: 0, width: asset.width ?? width, height: asset.height ?? height };
1516
- const canvasW = asset.width ?? width;
1517
- const canvasH = asset.height ?? height;
1518
- const canvasPR = asset.pixelRatio ?? pixelRatio;
1519
- const ops0 = await buildDrawOps({
1520
- canvas: { width: canvasW, height: canvasH, pixelRatio: canvasPR },
1521
- textRect,
1522
- lines,
1523
- font: {
1524
- family: main.family,
1525
- size: main.size,
1526
- weight: `${main.weight}`,
1527
- style: main.style,
1528
- color: main.color,
1529
- opacity: main.opacity
1530
- },
1531
- style: {
1532
- lineHeight: asset.style?.lineHeight ?? 1.2,
1533
- textDecoration: asset.style?.textDecoration ?? "none",
1534
- gradient: asset.style?.gradient
1535
- },
1536
- stroke: asset.stroke,
1537
- shadow: asset.shadow,
1538
- align: asset.align ?? { horizontal: "left", vertical: "middle" },
1539
- background: asset.background,
1540
- glyphPathProvider: (gid) => fonts.glyphPath(desc, gid),
1541
- getUnitsPerEm: () => fonts.getUnitsPerEm(desc)
1542
- });
1543
- const ops = applyAnimation(ops0, lines, {
1544
- t: tSeconds,
1545
- fontSize: main.size,
1546
- anim: asset.animation ? {
1547
- preset: asset.animation.preset,
1548
- speed: asset.animation.speed,
1549
- duration: asset.animation.duration,
1550
- style: asset.animation.style,
1551
- direction: asset.animation.direction
1552
- } : void 0
1553
- });
1554
- return ops;
1718
+ try {
1719
+ const main = await ensureFonts(asset);
1720
+ const desc = { family: main.family, weight: `${main.weight}`, style: main.style };
1721
+ let lines;
1722
+ try {
1723
+ lines = await layout.layout({
1724
+ text: asset.text,
1725
+ width: asset.width ?? width,
1726
+ letterSpacing: asset.style?.letterSpacing ?? 0,
1727
+ fontSize: main.size,
1728
+ lineHeight: asset.style?.lineHeight ?? 1.2,
1729
+ desc,
1730
+ textTransform: asset.style?.textTransform ?? "none"
1731
+ });
1732
+ } catch (err) {
1733
+ throw new Error(
1734
+ `Failed to layout text: ${err instanceof Error ? err.message : String(err)}`
1735
+ );
1736
+ }
1737
+ const textRect = {
1738
+ x: 0,
1739
+ y: 0,
1740
+ width: asset.width ?? width,
1741
+ height: asset.height ?? height
1742
+ };
1743
+ const canvasW = asset.width ?? width;
1744
+ const canvasH = asset.height ?? height;
1745
+ const canvasPR = asset.pixelRatio ?? pixelRatio;
1746
+ let ops0;
1747
+ try {
1748
+ ops0 = await buildDrawOps({
1749
+ canvas: { width: canvasW, height: canvasH, pixelRatio: canvasPR },
1750
+ textRect,
1751
+ lines,
1752
+ font: {
1753
+ family: main.family,
1754
+ size: main.size,
1755
+ weight: `${main.weight}`,
1756
+ style: main.style,
1757
+ color: main.color,
1758
+ opacity: main.opacity
1759
+ },
1760
+ style: {
1761
+ lineHeight: asset.style?.lineHeight ?? 1.2,
1762
+ textDecoration: asset.style?.textDecoration ?? "none",
1763
+ gradient: asset.style?.gradient
1764
+ },
1765
+ stroke: asset.stroke,
1766
+ shadow: asset.shadow,
1767
+ align: asset.align ?? { horizontal: "left", vertical: "middle" },
1768
+ background: asset.background,
1769
+ glyphPathProvider: (gid) => fonts.glyphPath(desc, gid),
1770
+ getUnitsPerEm: () => fonts.getUnitsPerEm(desc)
1771
+ });
1772
+ } catch (err) {
1773
+ throw new Error(
1774
+ `Failed to build draw operations: ${err instanceof Error ? err.message : String(err)}`
1775
+ );
1776
+ }
1777
+ try {
1778
+ const ops = applyAnimation(ops0, lines, {
1779
+ t: tSeconds,
1780
+ fontSize: main.size,
1781
+ anim: asset.animation ? {
1782
+ preset: asset.animation.preset,
1783
+ speed: asset.animation.speed,
1784
+ duration: asset.animation.duration,
1785
+ style: asset.animation.style,
1786
+ direction: asset.animation.direction
1787
+ } : void 0
1788
+ });
1789
+ return ops;
1790
+ } catch (err) {
1791
+ throw new Error(
1792
+ `Failed to apply animation: ${err instanceof Error ? err.message : String(err)}`
1793
+ );
1794
+ }
1795
+ } catch (err) {
1796
+ if (err instanceof Error) {
1797
+ throw err;
1798
+ }
1799
+ throw new Error(`Failed to render frame at time ${tSeconds}s: ${String(err)}`);
1800
+ }
1555
1801
  },
1556
1802
  async createRenderer(p) {
1557
- return createNodePainter({
1558
- width: p.width ?? width,
1559
- height: p.height ?? height,
1560
- pixelRatio: p.pixelRatio ?? pixelRatio
1561
- });
1803
+ try {
1804
+ return await createNodePainter({
1805
+ width: p.width ?? width,
1806
+ height: p.height ?? height,
1807
+ pixelRatio: p.pixelRatio ?? pixelRatio
1808
+ });
1809
+ } catch (err) {
1810
+ throw new Error(
1811
+ `Failed to create renderer: ${err instanceof Error ? err.message : String(err)}`
1812
+ );
1813
+ }
1562
1814
  },
1563
1815
  async generateVideo(asset, options) {
1564
- const finalOptions = {
1565
- width: asset.width ?? width,
1566
- height: asset.height ?? height,
1567
- fps,
1568
- duration: asset.animation?.duration ?? 3,
1569
- outputPath: options.outputPath ?? "output.mp4",
1570
- pixelRatio: asset.pixelRatio ?? pixelRatio
1571
- };
1572
- const frameGenerator = async (time) => {
1573
- return this.renderFrame(asset, time);
1574
- };
1575
- await videoGenerator.generateVideo(frameGenerator, finalOptions);
1816
+ try {
1817
+ const finalOptions = {
1818
+ width: asset.width ?? width,
1819
+ height: asset.height ?? height,
1820
+ fps,
1821
+ duration: asset.animation?.duration ?? 3,
1822
+ outputPath: options.outputPath ?? "output.mp4",
1823
+ pixelRatio: asset.pixelRatio ?? pixelRatio
1824
+ };
1825
+ const frameGenerator = async (time) => {
1826
+ return this.renderFrame(asset, time);
1827
+ };
1828
+ await videoGenerator.generateVideo(frameGenerator, finalOptions);
1829
+ } catch (err) {
1830
+ throw new Error(
1831
+ `Failed to generate video: ${err instanceof Error ? err.message : String(err)}`
1832
+ );
1833
+ }
1576
1834
  },
1577
1835
  destroy() {
1578
- fonts.destroy();
1836
+ try {
1837
+ fonts.destroy();
1838
+ } catch (err) {
1839
+ console.error(`Error during cleanup: ${err instanceof Error ? err.message : String(err)}`);
1840
+ }
1579
1841
  }
1580
1842
  };
1581
1843
  }
1582
1844
  // Annotate the CommonJS export names for ESM import in node:
1583
1845
  0 && (module.exports = {
1584
- createTextEngine
1846
+ createTextEngine,
1847
+ isGlyphFill,
1848
+ isShadowFill
1585
1849
  });
1586
1850
  //# sourceMappingURL=entry.node.cjs.map