@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.
@@ -128,31 +128,28 @@ var RichTextAssetSchema = Joi.object({
128
128
  var hbSingleton = null;
129
129
  async function initHB(wasmBaseURL) {
130
130
  if (hbSingleton) return hbSingleton;
131
- const harfbuzzjs = await import("harfbuzzjs");
132
- const factory = harfbuzzjs.default;
133
- let hbUrlFromImport;
134
131
  try {
135
- hbUrlFromImport = (await import("harfbuzzjs/hb.wasm?url")).default;
136
- } catch {
137
- }
138
- const base = (() => {
139
- if (wasmBaseURL) return wasmBaseURL.replace(/\/$/, "");
140
- if (hbUrlFromImport) return hbUrlFromImport.replace(/hb\.wasm(?:\?.*)?$/, "");
141
- return new URL(".", import.meta.url).toString().replace(/\/$/, "");
142
- })();
143
- const locateFile = (path, scriptDir) => {
144
- if (path.endsWith(".wasm")) {
145
- return `${base}/${path}`.replace(/([^:])\/{2,}/g, "$1/");
132
+ const mod = await import("harfbuzzjs");
133
+ const candidate = mod.default;
134
+ let hb;
135
+ if (typeof candidate === "function") {
136
+ hb = await candidate();
137
+ } else if (candidate && typeof candidate.then === "function") {
138
+ hb = await candidate;
139
+ } else {
140
+ hb = candidate;
146
141
  }
147
- return scriptDir ? scriptDir + path : path;
148
- };
149
- const initArg = { locateFile };
150
- const maybePromise = typeof factory === "function" ? factory(initArg) : factory;
151
- hbSingleton = maybePromise && typeof maybePromise.then === "function" ? await maybePromise : maybePromise;
152
- if (!hbSingleton || typeof hbSingleton.createBuffer !== "function") {
153
- throw new Error("Failed to initialize HarfBuzz: invalid API");
142
+ if (!hb || typeof hb.createBuffer !== "function" || typeof hb.createFont !== "function") {
143
+ throw new Error("Failed to initialize HarfBuzz: unexpected export shape from 'harfbuzzjs'.");
144
+ }
145
+ void wasmBaseURL;
146
+ hbSingleton = hb;
147
+ return hbSingleton;
148
+ } catch (err) {
149
+ throw new Error(
150
+ `Failed to initialize HarfBuzz: ${err instanceof Error ? err.message : String(err)}`
151
+ );
154
152
  }
155
- return hbSingleton;
156
153
  }
157
154
 
158
155
  // src/core/font-registry.ts
@@ -175,7 +172,13 @@ var FontRegistry = class {
175
172
  return;
176
173
  }
177
174
  this.initPromise = this._doInit();
178
- await this.initPromise;
175
+ try {
176
+ await this.initPromise;
177
+ } catch (err) {
178
+ throw new Error(
179
+ `Failed to initialize FontRegistry: ${err instanceof Error ? err.message : String(err)}`
180
+ );
181
+ }
179
182
  }
180
183
  async _doInit() {
181
184
  try {
@@ -187,7 +190,13 @@ var FontRegistry = class {
187
190
  }
188
191
  async getHB() {
189
192
  if (!this.hb) {
190
- await this.init();
193
+ try {
194
+ await this.init();
195
+ } catch (err) {
196
+ throw new Error(
197
+ `Failed to get HarfBuzz instance: ${err instanceof Error ? err.message : String(err)}`
198
+ );
199
+ }
191
200
  }
192
201
  return this.hb;
193
202
  }
@@ -195,48 +204,111 @@ var FontRegistry = class {
195
204
  return `${desc.family}__${desc.weight ?? "400"}__${desc.style ?? "normal"}`;
196
205
  }
197
206
  async registerFromBytes(bytes, desc) {
198
- if (!this.hb) await this.init();
199
- const k = this.key(desc);
200
- if (this.fonts.has(k)) return;
201
- const blob = this.hb.createBlob(bytes);
202
- const face = this.hb.createFace(blob, 0);
203
- const font = this.hb.createFont(face);
204
- const upem = face.upem || 1e3;
205
- font.setScale(upem, upem);
206
- this.blobs.set(k, blob);
207
- this.faces.set(k, face);
208
- this.fonts.set(k, font);
207
+ try {
208
+ if (!this.hb) await this.init();
209
+ const k = this.key(desc);
210
+ if (this.fonts.has(k)) return;
211
+ const blob = this.hb.createBlob(bytes);
212
+ const face = this.hb.createFace(blob, 0);
213
+ const font = this.hb.createFont(face);
214
+ const upem = face.upem || 1e3;
215
+ font.setScale(upem, upem);
216
+ this.blobs.set(k, blob);
217
+ this.faces.set(k, face);
218
+ this.fonts.set(k, font);
219
+ } catch (err) {
220
+ throw new Error(
221
+ `Failed to register font "${desc.family}": ${err instanceof Error ? err.message : String(err)}`
222
+ );
223
+ }
209
224
  }
210
225
  async getFont(desc) {
211
- if (!this.hb) await this.init();
212
- const k = this.key(desc);
213
- const f = this.fonts.get(k);
214
- if (!f) throw new Error(`Font not registered for ${k}`);
215
- return f;
226
+ try {
227
+ if (!this.hb) await this.init();
228
+ const k = this.key(desc);
229
+ const f = this.fonts.get(k);
230
+ if (!f) throw new Error(`Font not registered for ${k}`);
231
+ return f;
232
+ } catch (err) {
233
+ if (err instanceof Error && err.message.includes("Font not registered")) {
234
+ throw err;
235
+ }
236
+ throw new Error(
237
+ `Failed to get font "${desc.family}": ${err instanceof Error ? err.message : String(err)}`
238
+ );
239
+ }
216
240
  }
217
241
  async getFace(desc) {
218
- if (!this.hb) await this.init();
219
- const k = this.key(desc);
220
- return this.faces.get(k);
242
+ try {
243
+ if (!this.hb) await this.init();
244
+ const k = this.key(desc);
245
+ return this.faces.get(k);
246
+ } catch (err) {
247
+ throw new Error(
248
+ `Failed to get face for "${desc.family}": ${err instanceof Error ? err.message : String(err)}`
249
+ );
250
+ }
221
251
  }
222
252
  async getUnitsPerEm(desc) {
223
- const face = await this.getFace(desc);
224
- return face?.upem || 1e3;
253
+ try {
254
+ const face = await this.getFace(desc);
255
+ return face?.upem || 1e3;
256
+ } catch (err) {
257
+ throw new Error(
258
+ `Failed to get units per em for "${desc.family}": ${err instanceof Error ? err.message : String(err)}`
259
+ );
260
+ }
225
261
  }
226
262
  async glyphPath(desc, glyphId) {
227
- const font = await this.getFont(desc);
228
- const path = font.glyphToPath(glyphId);
229
- return path && path !== "" ? path : "M 0 0";
263
+ try {
264
+ const font = await this.getFont(desc);
265
+ const path = font.glyphToPath(glyphId);
266
+ return path && path !== "" ? path : "M 0 0";
267
+ } catch (err) {
268
+ throw new Error(
269
+ `Failed to get glyph path for glyph ${glyphId} in font "${desc.family}": ${err instanceof Error ? err.message : String(err)}`
270
+ );
271
+ }
230
272
  }
231
273
  destroy() {
232
- for (const [, f] of this.fonts) f.destroy?.();
233
- for (const [, f] of this.faces) f.destroy?.();
234
- for (const [, b] of this.blobs) b.destroy?.();
235
- this.fonts.clear();
236
- this.faces.clear();
237
- this.blobs.clear();
238
- this.hb = void 0;
239
- this.initPromise = void 0;
274
+ try {
275
+ for (const [, f] of this.fonts) {
276
+ try {
277
+ f.destroy();
278
+ } catch (err) {
279
+ console.error(
280
+ `Error destroying font: ${err instanceof Error ? err.message : String(err)}`
281
+ );
282
+ }
283
+ }
284
+ for (const [, f] of this.faces) {
285
+ try {
286
+ f.destroy();
287
+ } catch (err) {
288
+ console.error(
289
+ `Error destroying face: ${err instanceof Error ? err.message : String(err)}`
290
+ );
291
+ }
292
+ }
293
+ for (const [, b] of this.blobs) {
294
+ try {
295
+ b.destroy();
296
+ } catch (err) {
297
+ console.error(
298
+ `Error destroying blob: ${err instanceof Error ? err.message : String(err)}`
299
+ );
300
+ }
301
+ }
302
+ this.fonts.clear();
303
+ this.faces.clear();
304
+ this.blobs.clear();
305
+ this.hb = void 0;
306
+ this.initPromise = void 0;
307
+ } catch (err) {
308
+ console.error(
309
+ `Error during FontRegistry cleanup: ${err instanceof Error ? err.message : String(err)}`
310
+ );
311
+ }
240
312
  }
241
313
  };
242
314
 
@@ -258,112 +330,145 @@ var LayoutEngine = class {
258
330
  }
259
331
  }
260
332
  async shapeFull(text, desc) {
261
- const hb = await this.fonts.getHB();
262
- const buffer = hb.createBuffer();
263
- buffer.addText(text);
264
- buffer.guessSegmentProperties();
265
- const font = await this.fonts.getFont(desc);
266
- const face = await this.fonts.getFace(desc);
267
- const upem = face?.upem || 1e3;
268
- font.setScale(upem, upem);
269
- hb.shape(font, buffer);
270
- const result = buffer.json();
271
- buffer.destroy();
272
- return result;
333
+ try {
334
+ const hb = await this.fonts.getHB();
335
+ const buffer = hb.createBuffer();
336
+ try {
337
+ buffer.addText(text);
338
+ buffer.guessSegmentProperties();
339
+ const font = await this.fonts.getFont(desc);
340
+ const face = await this.fonts.getFace(desc);
341
+ const upem = face?.upem || 1e3;
342
+ font.setScale(upem, upem);
343
+ hb.shape(font, buffer);
344
+ const result = buffer.json();
345
+ return result;
346
+ } finally {
347
+ try {
348
+ buffer.destroy();
349
+ } catch (err) {
350
+ console.error(
351
+ `Error destroying HarfBuzz buffer: ${err instanceof Error ? err.message : String(err)}`
352
+ );
353
+ }
354
+ }
355
+ } catch (err) {
356
+ throw new Error(
357
+ `Failed to shape text with font "${desc.family}": ${err instanceof Error ? err.message : String(err)}`
358
+ );
359
+ }
273
360
  }
274
361
  async layout(params) {
275
- const { textTransform, desc, fontSize, letterSpacing, width } = params;
276
- const input = this.transformText(params.text, textTransform);
277
- if (!input || input.length === 0) {
278
- return [];
279
- }
280
- const shaped = await this.shapeFull(input, desc);
281
- const face = await this.fonts.getFace(desc);
282
- const upem = face?.upem || 1e3;
283
- const scale = fontSize / upem;
284
- const glyphs = shaped.map((g) => {
285
- const charIndex = g.cl;
286
- let char;
287
- if (charIndex >= 0 && charIndex < input.length) {
288
- char = input[charIndex];
362
+ try {
363
+ const { textTransform, desc, fontSize, letterSpacing, width } = params;
364
+ const input = this.transformText(params.text, textTransform);
365
+ if (!input || input.length === 0) {
366
+ return [];
289
367
  }
290
- return {
291
- id: g.g,
292
- xAdvance: g.ax * scale + letterSpacing,
293
- xOffset: g.dx * scale,
294
- yOffset: -g.dy * scale,
295
- cluster: g.cl,
296
- char
297
- // This now correctly maps to the original character
298
- };
299
- });
300
- const lines = [];
301
- let currentLine = [];
302
- let currentWidth = 0;
303
- const spaceIndices = /* @__PURE__ */ new Set();
304
- for (let i = 0; i < input.length; i++) {
305
- if (input[i] === " ") {
306
- spaceIndices.add(i);
368
+ let shaped;
369
+ try {
370
+ shaped = await this.shapeFull(input, desc);
371
+ } catch (err) {
372
+ throw new Error(`Text shaping failed: ${err instanceof Error ? err.message : String(err)}`);
307
373
  }
308
- }
309
- let lastBreakIndex = -1;
310
- for (let i = 0; i < glyphs.length; i++) {
311
- const glyph = glyphs[i];
312
- const glyphWidth = glyph.xAdvance;
313
- if (glyph.char === "\n") {
314
- if (currentLine.length > 0) {
315
- lines.push({
316
- glyphs: currentLine,
317
- width: currentWidth,
318
- y: 0
319
- });
374
+ let upem;
375
+ try {
376
+ const face = await this.fonts.getFace(desc);
377
+ upem = face?.upem || 1e3;
378
+ } catch (err) {
379
+ throw new Error(
380
+ `Failed to get font metrics: ${err instanceof Error ? err.message : String(err)}`
381
+ );
382
+ }
383
+ const scale = fontSize / upem;
384
+ const glyphs = shaped.map((g) => {
385
+ const charIndex = g.cl;
386
+ let char;
387
+ if (charIndex >= 0 && charIndex < input.length) {
388
+ char = input[charIndex];
389
+ }
390
+ return {
391
+ id: g.g,
392
+ xAdvance: g.ax * scale + letterSpacing,
393
+ xOffset: g.dx * scale,
394
+ yOffset: -g.dy * scale,
395
+ cluster: g.cl,
396
+ char
397
+ };
398
+ });
399
+ const lines = [];
400
+ let currentLine = [];
401
+ let currentWidth = 0;
402
+ const spaceIndices = /* @__PURE__ */ new Set();
403
+ for (let i = 0; i < input.length; i++) {
404
+ if (input[i] === " ") {
405
+ spaceIndices.add(i);
320
406
  }
321
- currentLine = [];
322
- currentWidth = 0;
323
- lastBreakIndex = i;
324
- continue;
325
407
  }
326
- if (currentWidth + glyphWidth > width && currentLine.length > 0) {
327
- if (lastBreakIndex > -1) {
328
- const breakPoint = lastBreakIndex - (i - currentLine.length) + 1;
329
- const nextLine = currentLine.splice(breakPoint);
330
- const lineWidth = currentLine.reduce((sum, g) => sum + g.xAdvance, 0);
331
- lines.push({
332
- glyphs: currentLine,
333
- width: lineWidth,
334
- y: 0
335
- });
336
- currentLine = nextLine;
337
- currentWidth = nextLine.reduce((sum, g) => sum + g.xAdvance, 0);
338
- } else {
339
- lines.push({
340
- glyphs: currentLine,
341
- width: currentWidth,
342
- y: 0
343
- });
408
+ let lastBreakIndex = -1;
409
+ for (let i = 0; i < glyphs.length; i++) {
410
+ const glyph = glyphs[i];
411
+ const glyphWidth = glyph.xAdvance;
412
+ if (glyph.char === "\n") {
413
+ if (currentLine.length > 0) {
414
+ lines.push({
415
+ glyphs: currentLine,
416
+ width: currentWidth,
417
+ y: 0
418
+ });
419
+ }
344
420
  currentLine = [];
345
421
  currentWidth = 0;
422
+ lastBreakIndex = i;
423
+ continue;
424
+ }
425
+ if (currentWidth + glyphWidth > width && currentLine.length > 0) {
426
+ if (lastBreakIndex > -1) {
427
+ const breakPoint = lastBreakIndex - (i - currentLine.length) + 1;
428
+ const nextLine = currentLine.splice(breakPoint);
429
+ const lineWidth = currentLine.reduce((sum, g) => sum + g.xAdvance, 0);
430
+ lines.push({
431
+ glyphs: currentLine,
432
+ width: lineWidth,
433
+ y: 0
434
+ });
435
+ currentLine = nextLine;
436
+ currentWidth = nextLine.reduce((sum, g) => sum + g.xAdvance, 0);
437
+ } else {
438
+ lines.push({
439
+ glyphs: currentLine,
440
+ width: currentWidth,
441
+ y: 0
442
+ });
443
+ currentLine = [];
444
+ currentWidth = 0;
445
+ }
446
+ lastBreakIndex = -1;
447
+ }
448
+ currentLine.push(glyph);
449
+ currentWidth += glyphWidth;
450
+ if (spaceIndices.has(glyph.cluster)) {
451
+ lastBreakIndex = i;
346
452
  }
347
- lastBreakIndex = -1;
348
453
  }
349
- currentLine.push(glyph);
350
- currentWidth += glyphWidth;
351
- if (spaceIndices.has(glyph.cluster)) {
352
- lastBreakIndex = i;
454
+ if (currentLine.length > 0) {
455
+ lines.push({
456
+ glyphs: currentLine,
457
+ width: currentWidth,
458
+ y: 0
459
+ });
353
460
  }
461
+ const lineHeight = params.lineHeight * fontSize;
462
+ for (let i = 0; i < lines.length; i++) {
463
+ lines[i].y = (i + 1) * lineHeight;
464
+ }
465
+ return lines;
466
+ } catch (err) {
467
+ if (err instanceof Error) {
468
+ throw err;
469
+ }
470
+ throw new Error(`Layout failed: ${String(err)}`);
354
471
  }
355
- if (currentLine.length > 0) {
356
- lines.push({
357
- glyphs: currentLine,
358
- width: currentWidth,
359
- y: 0
360
- });
361
- }
362
- const lineHeight = params.lineHeight * fontSize;
363
- for (let i = 0; i < lines.length; i++) {
364
- lines[i].y = (i + 1) * lineHeight;
365
- }
366
- return lines;
367
472
  }
368
473
  };
369
474
 
@@ -455,7 +560,6 @@ async function buildDrawOps(p) {
455
560
  path,
456
561
  x: glyphX + p.shadow.offsetX,
457
562
  y: glyphY + p.shadow.offsetY,
458
- // @ts-ignore scale propagated to painters
459
563
  scale,
460
564
  fill: { kind: "solid", color: p.shadow.color, opacity: p.shadow.opacity }
461
565
  });
@@ -466,7 +570,6 @@ async function buildDrawOps(p) {
466
570
  path,
467
571
  x: glyphX,
468
572
  y: glyphY,
469
- // @ts-ignore scale propagated to painters
470
573
  scale,
471
574
  width: p.stroke.width,
472
575
  color: p.stroke.color,
@@ -478,7 +581,6 @@ async function buildDrawOps(p) {
478
581
  path,
479
582
  x: glyphX,
480
583
  y: glyphY,
481
- // @ts-ignore scale propagated to painters
482
584
  scale,
483
585
  fill
484
586
  });
@@ -1054,30 +1156,32 @@ async function createNodePainter(opts) {
1054
1156
  continue;
1055
1157
  }
1056
1158
  if (op.op === "FillPath") {
1159
+ const fillOp = op;
1057
1160
  ctx.save();
1058
- ctx.translate(op.x, op.y);
1059
- const s = op.scale ?? 1;
1161
+ ctx.translate(fillOp.x, fillOp.y);
1162
+ const s = fillOp.scale ?? 1;
1060
1163
  ctx.scale(s, -s);
1061
1164
  ctx.beginPath();
1062
- drawSvgPathOnCtx(ctx, op.path);
1063
- const bbox = op.gradientBBox ?? globalBox;
1064
- const fill = makeGradientFromBBox(ctx, op.fill, bbox);
1165
+ drawSvgPathOnCtx(ctx, fillOp.path);
1166
+ const bbox = fillOp.gradientBBox ?? globalBox;
1167
+ const fill = makeGradientFromBBox(ctx, fillOp.fill, bbox);
1065
1168
  ctx.fillStyle = fill;
1066
1169
  ctx.fill();
1067
1170
  ctx.restore();
1068
1171
  continue;
1069
1172
  }
1070
1173
  if (op.op === "StrokePath") {
1174
+ const strokeOp = op;
1071
1175
  ctx.save();
1072
- ctx.translate(op.x, op.y);
1073
- const s = op.scale ?? 1;
1176
+ ctx.translate(strokeOp.x, strokeOp.y);
1177
+ const s = strokeOp.scale ?? 1;
1074
1178
  ctx.scale(s, -s);
1075
1179
  const invAbs = 1 / Math.abs(s);
1076
1180
  ctx.beginPath();
1077
- drawSvgPathOnCtx(ctx, op.path);
1078
- const c = parseHex6(op.color, op.opacity);
1181
+ drawSvgPathOnCtx(ctx, strokeOp.path);
1182
+ const c = parseHex6(strokeOp.color, strokeOp.opacity);
1079
1183
  ctx.strokeStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
1080
- ctx.lineWidth = op.width * invAbs;
1184
+ ctx.lineWidth = strokeOp.width * invAbs;
1081
1185
  ctx.lineJoin = "round";
1082
1186
  ctx.lineCap = "round";
1083
1187
  ctx.stroke();
@@ -1109,17 +1213,20 @@ function makeGradientFromBBox(ctx, spec, box) {
1109
1213
  const c = parseHex6(spec.color, spec.opacity);
1110
1214
  return `rgba(${c.r},${c.g},${c.b},${c.a})`;
1111
1215
  }
1112
- const cx = box.x + box.w / 2, cy = box.y + box.h / 2, r = Math.max(box.w, box.h) / 2;
1216
+ const cx = box.x + box.w / 2;
1217
+ const cy = box.y + box.h / 2;
1218
+ const r = Math.max(box.w, box.h) / 2;
1113
1219
  const addStops = (g) => {
1114
- const op = spec.opacity ?? 1;
1115
- for (const s of spec.stops) {
1116
- const c = parseHex6(s.color, op);
1220
+ const opacity = spec.kind === "linear" || spec.kind === "radial" ? spec.opacity : 1;
1221
+ const stops = spec.kind === "linear" || spec.kind === "radial" ? spec.stops : [];
1222
+ for (const s of stops) {
1223
+ const c = parseHex6(s.color, opacity);
1117
1224
  g.addColorStop(s.offset, `rgba(${c.r},${c.g},${c.b},${c.a})`);
1118
1225
  }
1119
1226
  return g;
1120
1227
  };
1121
1228
  if (spec.kind === "linear") {
1122
- const rad = (spec.angle || 0) * Math.PI / 180;
1229
+ const rad = spec.angle * Math.PI / 180;
1123
1230
  const x1 = cx + Math.cos(rad + Math.PI) * r;
1124
1231
  const y1 = cy + Math.sin(rad + Math.PI) * r;
1125
1232
  const x2 = cx + Math.cos(rad) * r;
@@ -1130,22 +1237,34 @@ function makeGradientFromBBox(ctx, spec, box) {
1130
1237
  }
1131
1238
  }
1132
1239
  function computeGlobalTextBounds(ops) {
1133
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1240
+ let minX = Infinity;
1241
+ let minY = Infinity;
1242
+ let maxX = -Infinity;
1243
+ let maxY = -Infinity;
1134
1244
  for (const op of ops) {
1135
- if (op.op !== "FillPath" || op.isShadow) continue;
1136
- const b = computePathBounds2(op.path);
1137
- const s = op.scale ?? 1;
1138
- const x1 = op.x + s * b.x;
1139
- const x2 = op.x + s * (b.x + b.w);
1140
- const y1 = op.y - s * (b.y + b.h);
1141
- const y2 = op.y - s * b.y;
1245
+ if (op.op !== "FillPath") continue;
1246
+ const fillOp = op;
1247
+ if (fillOp.isShadow) continue;
1248
+ const b = computePathBounds2(fillOp.path);
1249
+ const s = fillOp.scale ?? 1;
1250
+ const x1 = fillOp.x + s * b.x;
1251
+ const x2 = fillOp.x + s * (b.x + b.w);
1252
+ const y1 = fillOp.y - s * (b.y + b.h);
1253
+ const y2 = fillOp.y - s * b.y;
1142
1254
  if (x1 < minX) minX = x1;
1143
1255
  if (y1 < minY) minY = y1;
1144
1256
  if (x2 > maxX) maxX = x2;
1145
1257
  if (y2 > maxY) maxY = y2;
1146
1258
  }
1147
- if (minX === Infinity) return { x: 0, y: 0, w: 1, h: 1 };
1148
- return { x: minX, y: minY, w: Math.max(1, maxX - minX), h: Math.max(1, maxY - minY) };
1259
+ if (minX === Infinity) {
1260
+ return { x: 0, y: 0, w: 1, h: 1 };
1261
+ }
1262
+ return {
1263
+ x: minX,
1264
+ y: minY,
1265
+ w: Math.max(1, maxX - minX),
1266
+ h: Math.max(1, maxY - minY)
1267
+ };
1149
1268
  }
1150
1269
  function drawSvgPathOnCtx(ctx, d) {
1151
1270
  const t = tokenizePath2(d);
@@ -1187,13 +1306,18 @@ function drawSvgPathOnCtx(ctx, d) {
1187
1306
  ctx.closePath();
1188
1307
  break;
1189
1308
  }
1309
+ default:
1310
+ break;
1190
1311
  }
1191
1312
  }
1192
1313
  }
1193
1314
  function computePathBounds2(d) {
1194
1315
  const t = tokenizePath2(d);
1195
1316
  let i = 0;
1196
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1317
+ let minX = Infinity;
1318
+ let minY = Infinity;
1319
+ let maxX = -Infinity;
1320
+ let maxY = -Infinity;
1197
1321
  const touch = (x, y) => {
1198
1322
  if (x < minX) minX = x;
1199
1323
  if (y < minY) minY = y;
@@ -1233,10 +1357,19 @@ function computePathBounds2(d) {
1233
1357
  }
1234
1358
  case "Z":
1235
1359
  break;
1360
+ default:
1361
+ break;
1236
1362
  }
1237
1363
  }
1238
- if (minX === Infinity) return { x: 0, y: 0, w: 0, h: 0 };
1239
- return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
1364
+ if (minX === Infinity) {
1365
+ return { x: 0, y: 0, w: 0, h: 0 };
1366
+ }
1367
+ return {
1368
+ x: minX,
1369
+ y: minY,
1370
+ w: maxX - minX,
1371
+ h: maxY - minY
1372
+ };
1240
1373
  }
1241
1374
  function roundRectPath(ctx, x, y, w, h, r) {
1242
1375
  ctx.moveTo(x + r, y);
@@ -1265,20 +1398,54 @@ function bufferToArrayBuffer(buf) {
1265
1398
  return ab.slice(byteOffset, byteOffset + byteLength);
1266
1399
  }
1267
1400
  async function loadFileOrHttpToArrayBuffer(pathOrUrl) {
1268
- if (/^https?:\/\//.test(pathOrUrl)) {
1269
- const client = pathOrUrl.startsWith("https:") ? https : http;
1270
- const buf2 = await new Promise((resolve, reject) => {
1271
- client.get(pathOrUrl, (res) => {
1272
- const chunks = [];
1273
- res.on("data", (d) => chunks.push(d));
1274
- res.on("end", () => resolve(Buffer.concat(chunks)));
1275
- res.on("error", reject);
1276
- }).on("error", reject);
1277
- });
1278
- return bufferToArrayBuffer(buf2);
1401
+ try {
1402
+ if (/^https?:\/\//.test(pathOrUrl)) {
1403
+ const client = pathOrUrl.startsWith("https:") ? https : http;
1404
+ const buf2 = await new Promise((resolve, reject) => {
1405
+ const request = client.get(pathOrUrl, (res) => {
1406
+ const { statusCode } = res;
1407
+ if (statusCode && (statusCode < 200 || statusCode >= 300)) {
1408
+ reject(new Error(`HTTP request failed with status ${statusCode} for ${pathOrUrl}`));
1409
+ res.resume();
1410
+ return;
1411
+ }
1412
+ const chunks = [];
1413
+ res.on("data", (chunk) => {
1414
+ chunks.push(chunk);
1415
+ });
1416
+ res.on("end", () => {
1417
+ try {
1418
+ resolve(Buffer.concat(chunks));
1419
+ } catch (err) {
1420
+ reject(
1421
+ new Error(
1422
+ `Failed to concatenate response chunks: ${err instanceof Error ? err.message : String(err)}`
1423
+ )
1424
+ );
1425
+ }
1426
+ });
1427
+ res.on("error", (err) => {
1428
+ reject(new Error(`Response error for ${pathOrUrl}: ${err.message}`));
1429
+ });
1430
+ });
1431
+ request.on("error", (err) => {
1432
+ reject(new Error(`Request error for ${pathOrUrl}: ${err.message}`));
1433
+ });
1434
+ request.setTimeout(3e4, () => {
1435
+ request.destroy();
1436
+ reject(new Error(`Request timeout after 30s for ${pathOrUrl}`));
1437
+ });
1438
+ });
1439
+ return bufferToArrayBuffer(buf2);
1440
+ }
1441
+ const buf = await readFile(pathOrUrl);
1442
+ return bufferToArrayBuffer(buf);
1443
+ } catch (err) {
1444
+ if (err instanceof Error) {
1445
+ throw new Error(`Failed to load ${pathOrUrl}: ${err.message}`);
1446
+ }
1447
+ throw new Error(`Failed to load ${pathOrUrl}: ${String(err)}`);
1279
1448
  }
1280
- const buf = await readFile(pathOrUrl);
1281
- return bufferToArrayBuffer(buf);
1282
1449
  }
1283
1450
 
1284
1451
  // src/core/video-generator.ts
@@ -1414,6 +1581,14 @@ var VideoGenerator = class {
1414
1581
  }
1415
1582
  };
1416
1583
 
1584
+ // src/types.ts
1585
+ var isShadowFill2 = (op) => {
1586
+ return op.op === "FillPath" && op.isShadow === true;
1587
+ };
1588
+ var isGlyphFill2 = (op) => {
1589
+ return op.op === "FillPath" && op.isShadow !== true;
1590
+ };
1591
+
1417
1592
  // src/env/entry.node.ts
1418
1593
  async function createTextEngine(opts = {}) {
1419
1594
  const width = opts.width ?? CANVAS_CONFIG.DEFAULTS.width;
@@ -1424,125 +1599,213 @@ async function createTextEngine(opts = {}) {
1424
1599
  const fonts = new FontRegistry(wasmBaseURL);
1425
1600
  const layout = new LayoutEngine(fonts);
1426
1601
  const videoGenerator = new VideoGenerator();
1427
- await fonts.init();
1602
+ try {
1603
+ await fonts.init();
1604
+ } catch (err) {
1605
+ throw new Error(
1606
+ `Failed to initialize font registry: ${err instanceof Error ? err.message : String(err)}`
1607
+ );
1608
+ }
1428
1609
  async function ensureFonts(asset) {
1429
- if (asset.customFonts) {
1430
- for (const cf of asset.customFonts) {
1431
- const bytes = await loadFileOrHttpToArrayBuffer(cf.src);
1432
- await fonts.registerFromBytes(bytes, {
1433
- family: cf.family,
1434
- weight: cf.weight ?? "400",
1435
- style: cf.style ?? "normal"
1436
- });
1610
+ try {
1611
+ if (asset.customFonts) {
1612
+ for (const cf of asset.customFonts) {
1613
+ try {
1614
+ const bytes = await loadFileOrHttpToArrayBuffer(cf.src);
1615
+ await fonts.registerFromBytes(bytes, {
1616
+ family: cf.family,
1617
+ weight: cf.weight ?? "400",
1618
+ style: cf.style ?? "normal"
1619
+ });
1620
+ } catch (err) {
1621
+ throw new Error(
1622
+ `Failed to load custom font "${cf.family}" from ${cf.src}: ${err instanceof Error ? err.message : String(err)}`
1623
+ );
1624
+ }
1625
+ }
1437
1626
  }
1627
+ const main = asset.font ?? {
1628
+ family: "Roboto",
1629
+ weight: "400",
1630
+ style: "normal",
1631
+ size: 48,
1632
+ color: "#000000",
1633
+ opacity: 1
1634
+ };
1635
+ return main;
1636
+ } catch (err) {
1637
+ if (err instanceof Error) {
1638
+ throw err;
1639
+ }
1640
+ throw new Error(`Failed to ensure fonts: ${String(err)}`);
1438
1641
  }
1439
- const main = asset.font ?? {
1440
- family: "Roboto",
1441
- weight: "400",
1442
- style: "normal",
1443
- size: 48,
1444
- color: "#000000",
1445
- opacity: 1
1446
- };
1447
- return main;
1448
1642
  }
1449
1643
  return {
1450
1644
  validate(input) {
1451
- const { value, error } = RichTextAssetSchema.validate(input, {
1452
- abortEarly: false,
1453
- convert: true
1454
- });
1455
- if (error) throw error;
1456
- return { value };
1645
+ try {
1646
+ const { value, error } = RichTextAssetSchema.validate(input, {
1647
+ abortEarly: false,
1648
+ convert: true
1649
+ });
1650
+ if (error) throw error;
1651
+ return { value };
1652
+ } catch (err) {
1653
+ if (err instanceof Error) {
1654
+ throw new Error(`Validation failed: ${err.message}`);
1655
+ }
1656
+ throw new Error(`Validation failed: ${String(err)}`);
1657
+ }
1457
1658
  },
1458
1659
  async registerFontFromFile(path, desc) {
1459
- const bytes = await loadFileOrHttpToArrayBuffer(path);
1460
- await fonts.registerFromBytes(bytes, desc);
1660
+ try {
1661
+ const bytes = await loadFileOrHttpToArrayBuffer(path);
1662
+ await fonts.registerFromBytes(bytes, desc);
1663
+ } catch (err) {
1664
+ throw new Error(
1665
+ `Failed to register font "${desc.family}" from file ${path}: ${err instanceof Error ? err.message : String(err)}`
1666
+ );
1667
+ }
1461
1668
  },
1462
1669
  async registerFontFromUrl(url, desc) {
1463
- const bytes = await loadFileOrHttpToArrayBuffer(url);
1464
- await fonts.registerFromBytes(bytes, desc);
1670
+ try {
1671
+ const bytes = await loadFileOrHttpToArrayBuffer(url);
1672
+ await fonts.registerFromBytes(bytes, desc);
1673
+ } catch (err) {
1674
+ throw new Error(
1675
+ `Failed to register font "${desc.family}" from URL ${url}: ${err instanceof Error ? err.message : String(err)}`
1676
+ );
1677
+ }
1465
1678
  },
1466
1679
  async renderFrame(asset, tSeconds) {
1467
- const main = await ensureFonts(asset);
1468
- const desc = { family: main.family, weight: `${main.weight}`, style: main.style };
1469
- const lines = await layout.layout({
1470
- text: asset.text,
1471
- width: asset.width ?? width,
1472
- letterSpacing: asset.style?.letterSpacing ?? 0,
1473
- fontSize: main.size,
1474
- lineHeight: asset.style?.lineHeight ?? 1.2,
1475
- desc,
1476
- textTransform: asset.style?.textTransform ?? "none"
1477
- });
1478
- const textRect = { x: 0, y: 0, width: asset.width ?? width, height: asset.height ?? height };
1479
- const canvasW = asset.width ?? width;
1480
- const canvasH = asset.height ?? height;
1481
- const canvasPR = asset.pixelRatio ?? pixelRatio;
1482
- const ops0 = await buildDrawOps({
1483
- canvas: { width: canvasW, height: canvasH, pixelRatio: canvasPR },
1484
- textRect,
1485
- lines,
1486
- font: {
1487
- family: main.family,
1488
- size: main.size,
1489
- weight: `${main.weight}`,
1490
- style: main.style,
1491
- color: main.color,
1492
- opacity: main.opacity
1493
- },
1494
- style: {
1495
- lineHeight: asset.style?.lineHeight ?? 1.2,
1496
- textDecoration: asset.style?.textDecoration ?? "none",
1497
- gradient: asset.style?.gradient
1498
- },
1499
- stroke: asset.stroke,
1500
- shadow: asset.shadow,
1501
- align: asset.align ?? { horizontal: "left", vertical: "middle" },
1502
- background: asset.background,
1503
- glyphPathProvider: (gid) => fonts.glyphPath(desc, gid),
1504
- getUnitsPerEm: () => fonts.getUnitsPerEm(desc)
1505
- });
1506
- const ops = applyAnimation(ops0, lines, {
1507
- t: tSeconds,
1508
- fontSize: main.size,
1509
- anim: asset.animation ? {
1510
- preset: asset.animation.preset,
1511
- speed: asset.animation.speed,
1512
- duration: asset.animation.duration,
1513
- style: asset.animation.style,
1514
- direction: asset.animation.direction
1515
- } : void 0
1516
- });
1517
- return ops;
1680
+ try {
1681
+ const main = await ensureFonts(asset);
1682
+ const desc = { family: main.family, weight: `${main.weight}`, style: main.style };
1683
+ let lines;
1684
+ try {
1685
+ lines = await layout.layout({
1686
+ text: asset.text,
1687
+ width: asset.width ?? width,
1688
+ letterSpacing: asset.style?.letterSpacing ?? 0,
1689
+ fontSize: main.size,
1690
+ lineHeight: asset.style?.lineHeight ?? 1.2,
1691
+ desc,
1692
+ textTransform: asset.style?.textTransform ?? "none"
1693
+ });
1694
+ } catch (err) {
1695
+ throw new Error(
1696
+ `Failed to layout text: ${err instanceof Error ? err.message : String(err)}`
1697
+ );
1698
+ }
1699
+ const textRect = {
1700
+ x: 0,
1701
+ y: 0,
1702
+ width: asset.width ?? width,
1703
+ height: asset.height ?? height
1704
+ };
1705
+ const canvasW = asset.width ?? width;
1706
+ const canvasH = asset.height ?? height;
1707
+ const canvasPR = asset.pixelRatio ?? pixelRatio;
1708
+ let ops0;
1709
+ try {
1710
+ ops0 = await buildDrawOps({
1711
+ canvas: { width: canvasW, height: canvasH, pixelRatio: canvasPR },
1712
+ textRect,
1713
+ lines,
1714
+ font: {
1715
+ family: main.family,
1716
+ size: main.size,
1717
+ weight: `${main.weight}`,
1718
+ style: main.style,
1719
+ color: main.color,
1720
+ opacity: main.opacity
1721
+ },
1722
+ style: {
1723
+ lineHeight: asset.style?.lineHeight ?? 1.2,
1724
+ textDecoration: asset.style?.textDecoration ?? "none",
1725
+ gradient: asset.style?.gradient
1726
+ },
1727
+ stroke: asset.stroke,
1728
+ shadow: asset.shadow,
1729
+ align: asset.align ?? { horizontal: "left", vertical: "middle" },
1730
+ background: asset.background,
1731
+ glyphPathProvider: (gid) => fonts.glyphPath(desc, gid),
1732
+ getUnitsPerEm: () => fonts.getUnitsPerEm(desc)
1733
+ });
1734
+ } catch (err) {
1735
+ throw new Error(
1736
+ `Failed to build draw operations: ${err instanceof Error ? err.message : String(err)}`
1737
+ );
1738
+ }
1739
+ try {
1740
+ const ops = applyAnimation(ops0, lines, {
1741
+ t: tSeconds,
1742
+ fontSize: main.size,
1743
+ anim: asset.animation ? {
1744
+ preset: asset.animation.preset,
1745
+ speed: asset.animation.speed,
1746
+ duration: asset.animation.duration,
1747
+ style: asset.animation.style,
1748
+ direction: asset.animation.direction
1749
+ } : void 0
1750
+ });
1751
+ return ops;
1752
+ } catch (err) {
1753
+ throw new Error(
1754
+ `Failed to apply animation: ${err instanceof Error ? err.message : String(err)}`
1755
+ );
1756
+ }
1757
+ } catch (err) {
1758
+ if (err instanceof Error) {
1759
+ throw err;
1760
+ }
1761
+ throw new Error(`Failed to render frame at time ${tSeconds}s: ${String(err)}`);
1762
+ }
1518
1763
  },
1519
1764
  async createRenderer(p) {
1520
- return createNodePainter({
1521
- width: p.width ?? width,
1522
- height: p.height ?? height,
1523
- pixelRatio: p.pixelRatio ?? pixelRatio
1524
- });
1765
+ try {
1766
+ return await createNodePainter({
1767
+ width: p.width ?? width,
1768
+ height: p.height ?? height,
1769
+ pixelRatio: p.pixelRatio ?? pixelRatio
1770
+ });
1771
+ } catch (err) {
1772
+ throw new Error(
1773
+ `Failed to create renderer: ${err instanceof Error ? err.message : String(err)}`
1774
+ );
1775
+ }
1525
1776
  },
1526
1777
  async generateVideo(asset, options) {
1527
- const finalOptions = {
1528
- width: asset.width ?? width,
1529
- height: asset.height ?? height,
1530
- fps,
1531
- duration: asset.animation?.duration ?? 3,
1532
- outputPath: options.outputPath ?? "output.mp4",
1533
- pixelRatio: asset.pixelRatio ?? pixelRatio
1534
- };
1535
- const frameGenerator = async (time) => {
1536
- return this.renderFrame(asset, time);
1537
- };
1538
- await videoGenerator.generateVideo(frameGenerator, finalOptions);
1778
+ try {
1779
+ const finalOptions = {
1780
+ width: asset.width ?? width,
1781
+ height: asset.height ?? height,
1782
+ fps,
1783
+ duration: asset.animation?.duration ?? 3,
1784
+ outputPath: options.outputPath ?? "output.mp4",
1785
+ pixelRatio: asset.pixelRatio ?? pixelRatio
1786
+ };
1787
+ const frameGenerator = async (time) => {
1788
+ return this.renderFrame(asset, time);
1789
+ };
1790
+ await videoGenerator.generateVideo(frameGenerator, finalOptions);
1791
+ } catch (err) {
1792
+ throw new Error(
1793
+ `Failed to generate video: ${err instanceof Error ? err.message : String(err)}`
1794
+ );
1795
+ }
1539
1796
  },
1540
1797
  destroy() {
1541
- fonts.destroy();
1798
+ try {
1799
+ fonts.destroy();
1800
+ } catch (err) {
1801
+ console.error(`Error during cleanup: ${err instanceof Error ? err.message : String(err)}`);
1802
+ }
1542
1803
  }
1543
1804
  };
1544
1805
  }
1545
1806
  export {
1546
- createTextEngine
1807
+ createTextEngine,
1808
+ isGlyphFill2 as isGlyphFill,
1809
+ isShadowFill2 as isShadowFill
1547
1810
  };
1548
1811
  //# sourceMappingURL=entry.node.js.map