@shotstack/shotstack-canvas 1.1.0 → 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
 
@@ -164,19 +166,28 @@ var RichTextAssetSchema = import_joi.default.object({
164
166
  var hbSingleton = null;
165
167
  async function initHB(wasmBaseURL) {
166
168
  if (hbSingleton) return hbSingleton;
167
- 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;
175
- }
176
- if (!hbSingleton || typeof hbSingleton.createBuffer !== "function") {
177
- throw new Error("Failed to initialize HarfBuzz: invalid API");
169
+ try {
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;
179
+ }
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
+ );
178
190
  }
179
- return hbSingleton;
180
191
  }
181
192
 
182
193
  // src/core/font-registry.ts
@@ -199,7 +210,13 @@ var FontRegistry = class {
199
210
  return;
200
211
  }
201
212
  this.initPromise = this._doInit();
202
- 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
+ }
203
220
  }
204
221
  async _doInit() {
205
222
  try {
@@ -211,7 +228,13 @@ var FontRegistry = class {
211
228
  }
212
229
  async getHB() {
213
230
  if (!this.hb) {
214
- 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
+ }
215
238
  }
216
239
  return this.hb;
217
240
  }
@@ -219,48 +242,111 @@ var FontRegistry = class {
219
242
  return `${desc.family}__${desc.weight ?? "400"}__${desc.style ?? "normal"}`;
220
243
  }
221
244
  async registerFromBytes(bytes, desc) {
222
- if (!this.hb) await this.init();
223
- const k = this.key(desc);
224
- if (this.fonts.has(k)) return;
225
- const blob = this.hb.createBlob(bytes);
226
- const face = this.hb.createFace(blob, 0);
227
- const font = this.hb.createFont(face);
228
- const upem = face.upem || 1e3;
229
- font.setScale(upem, upem);
230
- this.blobs.set(k, blob);
231
- this.faces.set(k, face);
232
- 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
+ }
233
262
  }
234
263
  async getFont(desc) {
235
- if (!this.hb) await this.init();
236
- const k = this.key(desc);
237
- const f = this.fonts.get(k);
238
- if (!f) throw new Error(`Font not registered for ${k}`);
239
- 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
+ }
240
278
  }
241
279
  async getFace(desc) {
242
- if (!this.hb) await this.init();
243
- const k = this.key(desc);
244
- 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
+ }
245
289
  }
246
290
  async getUnitsPerEm(desc) {
247
- const face = await this.getFace(desc);
248
- 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
+ }
249
299
  }
250
300
  async glyphPath(desc, glyphId) {
251
- const font = await this.getFont(desc);
252
- const path = font.glyphToPath(glyphId);
253
- 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
+ }
254
310
  }
255
311
  destroy() {
256
- for (const [, f] of this.fonts) f.destroy?.();
257
- for (const [, f] of this.faces) f.destroy?.();
258
- for (const [, b] of this.blobs) b.destroy?.();
259
- this.fonts.clear();
260
- this.faces.clear();
261
- this.blobs.clear();
262
- this.hb = void 0;
263
- 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
+ }
264
350
  }
265
351
  };
266
352
 
@@ -282,112 +368,145 @@ var LayoutEngine = class {
282
368
  }
283
369
  }
284
370
  async shapeFull(text, desc) {
285
- const hb = await this.fonts.getHB();
286
- const buffer = hb.createBuffer();
287
- buffer.addText(text);
288
- buffer.guessSegmentProperties();
289
- const font = await this.fonts.getFont(desc);
290
- const face = await this.fonts.getFace(desc);
291
- const upem = face?.upem || 1e3;
292
- font.setScale(upem, upem);
293
- hb.shape(font, buffer);
294
- const result = buffer.json();
295
- buffer.destroy();
296
- 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
+ }
297
398
  }
298
399
  async layout(params) {
299
- const { textTransform, desc, fontSize, letterSpacing, width } = params;
300
- const input = this.transformText(params.text, textTransform);
301
- if (!input || input.length === 0) {
302
- return [];
303
- }
304
- const shaped = await this.shapeFull(input, desc);
305
- const face = await this.fonts.getFace(desc);
306
- const upem = face?.upem || 1e3;
307
- const scale = fontSize / upem;
308
- const glyphs = shaped.map((g) => {
309
- const charIndex = g.cl;
310
- let char;
311
- if (charIndex >= 0 && charIndex < input.length) {
312
- 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 [];
313
405
  }
314
- return {
315
- id: g.g,
316
- xAdvance: g.ax * scale + letterSpacing,
317
- xOffset: g.dx * scale,
318
- yOffset: -g.dy * scale,
319
- cluster: g.cl,
320
- char
321
- // This now correctly maps to the original character
322
- };
323
- });
324
- const lines = [];
325
- let currentLine = [];
326
- let currentWidth = 0;
327
- const spaceIndices = /* @__PURE__ */ new Set();
328
- for (let i = 0; i < input.length; i++) {
329
- if (input[i] === " ") {
330
- 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)}`);
331
411
  }
332
- }
333
- let lastBreakIndex = -1;
334
- for (let i = 0; i < glyphs.length; i++) {
335
- const glyph = glyphs[i];
336
- const glyphWidth = glyph.xAdvance;
337
- if (glyph.char === "\n") {
338
- if (currentLine.length > 0) {
339
- lines.push({
340
- glyphs: currentLine,
341
- width: currentWidth,
342
- y: 0
343
- });
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);
344
444
  }
345
- currentLine = [];
346
- currentWidth = 0;
347
- lastBreakIndex = i;
348
- continue;
349
445
  }
350
- if (currentWidth + glyphWidth > width && currentLine.length > 0) {
351
- if (lastBreakIndex > -1) {
352
- const breakPoint = lastBreakIndex - (i - currentLine.length) + 1;
353
- const nextLine = currentLine.splice(breakPoint);
354
- const lineWidth = currentLine.reduce((sum, g) => sum + g.xAdvance, 0);
355
- lines.push({
356
- glyphs: currentLine,
357
- width: lineWidth,
358
- y: 0
359
- });
360
- currentLine = nextLine;
361
- currentWidth = nextLine.reduce((sum, g) => sum + g.xAdvance, 0);
362
- } else {
363
- lines.push({
364
- glyphs: currentLine,
365
- width: currentWidth,
366
- y: 0
367
- });
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
+ }
368
458
  currentLine = [];
369
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;
370
490
  }
371
- lastBreakIndex = -1;
372
491
  }
373
- currentLine.push(glyph);
374
- currentWidth += glyphWidth;
375
- if (spaceIndices.has(glyph.cluster)) {
376
- lastBreakIndex = i;
492
+ if (currentLine.length > 0) {
493
+ lines.push({
494
+ glyphs: currentLine,
495
+ width: currentWidth,
496
+ y: 0
497
+ });
377
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)}`);
378
509
  }
379
- if (currentLine.length > 0) {
380
- lines.push({
381
- glyphs: currentLine,
382
- width: currentWidth,
383
- y: 0
384
- });
385
- }
386
- const lineHeight = params.lineHeight * fontSize;
387
- for (let i = 0; i < lines.length; i++) {
388
- lines[i].y = (i + 1) * lineHeight;
389
- }
390
- return lines;
391
510
  }
392
511
  };
393
512
 
@@ -479,7 +598,6 @@ async function buildDrawOps(p) {
479
598
  path,
480
599
  x: glyphX + p.shadow.offsetX,
481
600
  y: glyphY + p.shadow.offsetY,
482
- // @ts-ignore scale propagated to painters
483
601
  scale,
484
602
  fill: { kind: "solid", color: p.shadow.color, opacity: p.shadow.opacity }
485
603
  });
@@ -490,7 +608,6 @@ async function buildDrawOps(p) {
490
608
  path,
491
609
  x: glyphX,
492
610
  y: glyphY,
493
- // @ts-ignore scale propagated to painters
494
611
  scale,
495
612
  width: p.stroke.width,
496
613
  color: p.stroke.color,
@@ -502,7 +619,6 @@ async function buildDrawOps(p) {
502
619
  path,
503
620
  x: glyphX,
504
621
  y: glyphY,
505
- // @ts-ignore scale propagated to painters
506
622
  scale,
507
623
  fill
508
624
  });
@@ -1078,30 +1194,32 @@ async function createNodePainter(opts) {
1078
1194
  continue;
1079
1195
  }
1080
1196
  if (op.op === "FillPath") {
1197
+ const fillOp = op;
1081
1198
  ctx.save();
1082
- ctx.translate(op.x, op.y);
1083
- const s = op.scale ?? 1;
1199
+ ctx.translate(fillOp.x, fillOp.y);
1200
+ const s = fillOp.scale ?? 1;
1084
1201
  ctx.scale(s, -s);
1085
1202
  ctx.beginPath();
1086
- drawSvgPathOnCtx(ctx, op.path);
1087
- const bbox = op.gradientBBox ?? globalBox;
1088
- 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);
1089
1206
  ctx.fillStyle = fill;
1090
1207
  ctx.fill();
1091
1208
  ctx.restore();
1092
1209
  continue;
1093
1210
  }
1094
1211
  if (op.op === "StrokePath") {
1212
+ const strokeOp = op;
1095
1213
  ctx.save();
1096
- ctx.translate(op.x, op.y);
1097
- const s = op.scale ?? 1;
1214
+ ctx.translate(strokeOp.x, strokeOp.y);
1215
+ const s = strokeOp.scale ?? 1;
1098
1216
  ctx.scale(s, -s);
1099
1217
  const invAbs = 1 / Math.abs(s);
1100
1218
  ctx.beginPath();
1101
- drawSvgPathOnCtx(ctx, op.path);
1102
- const c = parseHex6(op.color, op.opacity);
1219
+ drawSvgPathOnCtx(ctx, strokeOp.path);
1220
+ const c = parseHex6(strokeOp.color, strokeOp.opacity);
1103
1221
  ctx.strokeStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
1104
- ctx.lineWidth = op.width * invAbs;
1222
+ ctx.lineWidth = strokeOp.width * invAbs;
1105
1223
  ctx.lineJoin = "round";
1106
1224
  ctx.lineCap = "round";
1107
1225
  ctx.stroke();
@@ -1133,17 +1251,20 @@ function makeGradientFromBBox(ctx, spec, box) {
1133
1251
  const c = parseHex6(spec.color, spec.opacity);
1134
1252
  return `rgba(${c.r},${c.g},${c.b},${c.a})`;
1135
1253
  }
1136
- 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;
1137
1257
  const addStops = (g) => {
1138
- const op = spec.opacity ?? 1;
1139
- for (const s of spec.stops) {
1140
- 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);
1141
1262
  g.addColorStop(s.offset, `rgba(${c.r},${c.g},${c.b},${c.a})`);
1142
1263
  }
1143
1264
  return g;
1144
1265
  };
1145
1266
  if (spec.kind === "linear") {
1146
- const rad = (spec.angle || 0) * Math.PI / 180;
1267
+ const rad = spec.angle * Math.PI / 180;
1147
1268
  const x1 = cx + Math.cos(rad + Math.PI) * r;
1148
1269
  const y1 = cy + Math.sin(rad + Math.PI) * r;
1149
1270
  const x2 = cx + Math.cos(rad) * r;
@@ -1154,22 +1275,34 @@ function makeGradientFromBBox(ctx, spec, box) {
1154
1275
  }
1155
1276
  }
1156
1277
  function computeGlobalTextBounds(ops) {
1157
- 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;
1158
1282
  for (const op of ops) {
1159
- if (op.op !== "FillPath" || op.isShadow) continue;
1160
- const b = computePathBounds2(op.path);
1161
- const s = op.scale ?? 1;
1162
- const x1 = op.x + s * b.x;
1163
- const x2 = op.x + s * (b.x + b.w);
1164
- const y1 = op.y - s * (b.y + b.h);
1165
- 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;
1166
1292
  if (x1 < minX) minX = x1;
1167
1293
  if (y1 < minY) minY = y1;
1168
1294
  if (x2 > maxX) maxX = x2;
1169
1295
  if (y2 > maxY) maxY = y2;
1170
1296
  }
1171
- if (minX === Infinity) return { x: 0, y: 0, w: 1, h: 1 };
1172
- 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
+ };
1173
1306
  }
1174
1307
  function drawSvgPathOnCtx(ctx, d) {
1175
1308
  const t = tokenizePath2(d);
@@ -1211,13 +1344,18 @@ function drawSvgPathOnCtx(ctx, d) {
1211
1344
  ctx.closePath();
1212
1345
  break;
1213
1346
  }
1347
+ default:
1348
+ break;
1214
1349
  }
1215
1350
  }
1216
1351
  }
1217
1352
  function computePathBounds2(d) {
1218
1353
  const t = tokenizePath2(d);
1219
1354
  let i = 0;
1220
- 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;
1221
1359
  const touch = (x, y) => {
1222
1360
  if (x < minX) minX = x;
1223
1361
  if (y < minY) minY = y;
@@ -1257,10 +1395,19 @@ function computePathBounds2(d) {
1257
1395
  }
1258
1396
  case "Z":
1259
1397
  break;
1398
+ default:
1399
+ break;
1260
1400
  }
1261
1401
  }
1262
- if (minX === Infinity) return { x: 0, y: 0, w: 0, h: 0 };
1263
- 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
+ };
1264
1411
  }
1265
1412
  function roundRectPath(ctx, x, y, w, h, r) {
1266
1413
  ctx.moveTo(x + r, y);
@@ -1289,20 +1436,54 @@ function bufferToArrayBuffer(buf) {
1289
1436
  return ab.slice(byteOffset, byteOffset + byteLength);
1290
1437
  }
1291
1438
  async function loadFileOrHttpToArrayBuffer(pathOrUrl) {
1292
- if (/^https?:\/\//.test(pathOrUrl)) {
1293
- const client = pathOrUrl.startsWith("https:") ? https : http;
1294
- const buf2 = await new Promise((resolve, reject) => {
1295
- client.get(pathOrUrl, (res) => {
1296
- const chunks = [];
1297
- res.on("data", (d) => chunks.push(d));
1298
- res.on("end", () => resolve(Buffer.concat(chunks)));
1299
- res.on("error", reject);
1300
- }).on("error", reject);
1301
- });
1302
- 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)}`);
1303
1486
  }
1304
- const buf = await (0, import_promises.readFile)(pathOrUrl);
1305
- return bufferToArrayBuffer(buf);
1306
1487
  }
1307
1488
 
1308
1489
  // src/core/video-generator.ts
@@ -1438,6 +1619,14 @@ var VideoGenerator = class {
1438
1619
  }
1439
1620
  };
1440
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
+
1441
1630
  // src/env/entry.node.ts
1442
1631
  async function createTextEngine(opts = {}) {
1443
1632
  const width = opts.width ?? CANVAS_CONFIG.DEFAULTS.width;
@@ -1448,126 +1637,214 @@ async function createTextEngine(opts = {}) {
1448
1637
  const fonts = new FontRegistry(wasmBaseURL);
1449
1638
  const layout = new LayoutEngine(fonts);
1450
1639
  const videoGenerator = new VideoGenerator();
1451
- 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
+ }
1452
1647
  async function ensureFonts(asset) {
1453
- if (asset.customFonts) {
1454
- for (const cf of asset.customFonts) {
1455
- const bytes = await loadFileOrHttpToArrayBuffer(cf.src);
1456
- await fonts.registerFromBytes(bytes, {
1457
- family: cf.family,
1458
- weight: cf.weight ?? "400",
1459
- style: cf.style ?? "normal"
1460
- });
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
+ }
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;
1461
1677
  }
1678
+ throw new Error(`Failed to ensure fonts: ${String(err)}`);
1462
1679
  }
1463
- const main = asset.font ?? {
1464
- family: "Roboto",
1465
- weight: "400",
1466
- style: "normal",
1467
- size: 48,
1468
- color: "#000000",
1469
- opacity: 1
1470
- };
1471
- return main;
1472
1680
  }
1473
1681
  return {
1474
1682
  validate(input) {
1475
- const { value, error } = RichTextAssetSchema.validate(input, {
1476
- abortEarly: false,
1477
- convert: true
1478
- });
1479
- if (error) throw error;
1480
- 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
+ }
1481
1696
  },
1482
1697
  async registerFontFromFile(path, desc) {
1483
- const bytes = await loadFileOrHttpToArrayBuffer(path);
1484
- 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
+ }
1485
1706
  },
1486
1707
  async registerFontFromUrl(url, desc) {
1487
- const bytes = await loadFileOrHttpToArrayBuffer(url);
1488
- 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
+ }
1489
1716
  },
1490
1717
  async renderFrame(asset, tSeconds) {
1491
- const main = await ensureFonts(asset);
1492
- const desc = { family: main.family, weight: `${main.weight}`, style: main.style };
1493
- const lines = await layout.layout({
1494
- text: asset.text,
1495
- width: asset.width ?? width,
1496
- letterSpacing: asset.style?.letterSpacing ?? 0,
1497
- fontSize: main.size,
1498
- lineHeight: asset.style?.lineHeight ?? 1.2,
1499
- desc,
1500
- textTransform: asset.style?.textTransform ?? "none"
1501
- });
1502
- const textRect = { x: 0, y: 0, width: asset.width ?? width, height: asset.height ?? height };
1503
- const canvasW = asset.width ?? width;
1504
- const canvasH = asset.height ?? height;
1505
- const canvasPR = asset.pixelRatio ?? pixelRatio;
1506
- const ops0 = await buildDrawOps({
1507
- canvas: { width: canvasW, height: canvasH, pixelRatio: canvasPR },
1508
- textRect,
1509
- lines,
1510
- font: {
1511
- family: main.family,
1512
- size: main.size,
1513
- weight: `${main.weight}`,
1514
- style: main.style,
1515
- color: main.color,
1516
- opacity: main.opacity
1517
- },
1518
- style: {
1519
- lineHeight: asset.style?.lineHeight ?? 1.2,
1520
- textDecoration: asset.style?.textDecoration ?? "none",
1521
- gradient: asset.style?.gradient
1522
- },
1523
- stroke: asset.stroke,
1524
- shadow: asset.shadow,
1525
- align: asset.align ?? { horizontal: "left", vertical: "middle" },
1526
- background: asset.background,
1527
- glyphPathProvider: (gid) => fonts.glyphPath(desc, gid),
1528
- getUnitsPerEm: () => fonts.getUnitsPerEm(desc)
1529
- });
1530
- const ops = applyAnimation(ops0, lines, {
1531
- t: tSeconds,
1532
- fontSize: main.size,
1533
- anim: asset.animation ? {
1534
- preset: asset.animation.preset,
1535
- speed: asset.animation.speed,
1536
- duration: asset.animation.duration,
1537
- style: asset.animation.style,
1538
- direction: asset.animation.direction
1539
- } : void 0
1540
- });
1541
- 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
+ }
1542
1801
  },
1543
1802
  async createRenderer(p) {
1544
- return createNodePainter({
1545
- width: p.width ?? width,
1546
- height: p.height ?? height,
1547
- pixelRatio: p.pixelRatio ?? pixelRatio
1548
- });
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
+ }
1549
1814
  },
1550
1815
  async generateVideo(asset, options) {
1551
- const finalOptions = {
1552
- width: asset.width ?? width,
1553
- height: asset.height ?? height,
1554
- fps,
1555
- duration: asset.animation?.duration ?? 3,
1556
- outputPath: options.outputPath ?? "output.mp4",
1557
- pixelRatio: asset.pixelRatio ?? pixelRatio
1558
- };
1559
- const frameGenerator = async (time) => {
1560
- return this.renderFrame(asset, time);
1561
- };
1562
- 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
+ }
1563
1834
  },
1564
1835
  destroy() {
1565
- fonts.destroy();
1836
+ try {
1837
+ fonts.destroy();
1838
+ } catch (err) {
1839
+ console.error(`Error during cleanup: ${err instanceof Error ? err.message : String(err)}`);
1840
+ }
1566
1841
  }
1567
1842
  };
1568
1843
  }
1569
1844
  // Annotate the CommonJS export names for ESM import in node:
1570
1845
  0 && (module.exports = {
1571
- createTextEngine
1846
+ createTextEngine,
1847
+ isGlyphFill,
1848
+ isShadowFill
1572
1849
  });
1573
1850
  //# sourceMappingURL=entry.node.cjs.map