@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.
@@ -128,19 +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 hbPromise = harfbuzzjs.default;
133
- if (typeof hbPromise === "function") {
134
- hbSingleton = await hbPromise();
135
- } else if (hbPromise && typeof hbPromise.then === "function") {
136
- hbSingleton = await hbPromise;
137
- } else {
138
- hbSingleton = hbPromise;
139
- }
140
- if (!hbSingleton || typeof hbSingleton.createBuffer !== "function") {
141
- throw new Error("Failed to initialize HarfBuzz: invalid API");
131
+ try {
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;
141
+ }
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
+ );
142
152
  }
143
- return hbSingleton;
144
153
  }
145
154
 
146
155
  // src/core/font-registry.ts
@@ -163,7 +172,13 @@ var FontRegistry = class {
163
172
  return;
164
173
  }
165
174
  this.initPromise = this._doInit();
166
- 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
+ }
167
182
  }
168
183
  async _doInit() {
169
184
  try {
@@ -175,7 +190,13 @@ var FontRegistry = class {
175
190
  }
176
191
  async getHB() {
177
192
  if (!this.hb) {
178
- 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
+ }
179
200
  }
180
201
  return this.hb;
181
202
  }
@@ -183,48 +204,111 @@ var FontRegistry = class {
183
204
  return `${desc.family}__${desc.weight ?? "400"}__${desc.style ?? "normal"}`;
184
205
  }
185
206
  async registerFromBytes(bytes, desc) {
186
- if (!this.hb) await this.init();
187
- const k = this.key(desc);
188
- if (this.fonts.has(k)) return;
189
- const blob = this.hb.createBlob(bytes);
190
- const face = this.hb.createFace(blob, 0);
191
- const font = this.hb.createFont(face);
192
- const upem = face.upem || 1e3;
193
- font.setScale(upem, upem);
194
- this.blobs.set(k, blob);
195
- this.faces.set(k, face);
196
- 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
+ }
197
224
  }
198
225
  async getFont(desc) {
199
- if (!this.hb) await this.init();
200
- const k = this.key(desc);
201
- const f = this.fonts.get(k);
202
- if (!f) throw new Error(`Font not registered for ${k}`);
203
- 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
+ }
204
240
  }
205
241
  async getFace(desc) {
206
- if (!this.hb) await this.init();
207
- const k = this.key(desc);
208
- 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
+ }
209
251
  }
210
252
  async getUnitsPerEm(desc) {
211
- const face = await this.getFace(desc);
212
- 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
+ }
213
261
  }
214
262
  async glyphPath(desc, glyphId) {
215
- const font = await this.getFont(desc);
216
- const path = font.glyphToPath(glyphId);
217
- 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
+ }
218
272
  }
219
273
  destroy() {
220
- for (const [, f] of this.fonts) f.destroy?.();
221
- for (const [, f] of this.faces) f.destroy?.();
222
- for (const [, b] of this.blobs) b.destroy?.();
223
- this.fonts.clear();
224
- this.faces.clear();
225
- this.blobs.clear();
226
- this.hb = void 0;
227
- 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
+ }
228
312
  }
229
313
  };
230
314
 
@@ -246,112 +330,145 @@ var LayoutEngine = class {
246
330
  }
247
331
  }
248
332
  async shapeFull(text, desc) {
249
- const hb = await this.fonts.getHB();
250
- const buffer = hb.createBuffer();
251
- buffer.addText(text);
252
- buffer.guessSegmentProperties();
253
- const font = await this.fonts.getFont(desc);
254
- const face = await this.fonts.getFace(desc);
255
- const upem = face?.upem || 1e3;
256
- font.setScale(upem, upem);
257
- hb.shape(font, buffer);
258
- const result = buffer.json();
259
- buffer.destroy();
260
- 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
+ }
261
360
  }
262
361
  async layout(params) {
263
- const { textTransform, desc, fontSize, letterSpacing, width } = params;
264
- const input = this.transformText(params.text, textTransform);
265
- if (!input || input.length === 0) {
266
- return [];
267
- }
268
- const shaped = await this.shapeFull(input, desc);
269
- const face = await this.fonts.getFace(desc);
270
- const upem = face?.upem || 1e3;
271
- const scale = fontSize / upem;
272
- const glyphs = shaped.map((g) => {
273
- const charIndex = g.cl;
274
- let char;
275
- if (charIndex >= 0 && charIndex < input.length) {
276
- 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 [];
277
367
  }
278
- return {
279
- id: g.g,
280
- xAdvance: g.ax * scale + letterSpacing,
281
- xOffset: g.dx * scale,
282
- yOffset: -g.dy * scale,
283
- cluster: g.cl,
284
- char
285
- // This now correctly maps to the original character
286
- };
287
- });
288
- const lines = [];
289
- let currentLine = [];
290
- let currentWidth = 0;
291
- const spaceIndices = /* @__PURE__ */ new Set();
292
- for (let i = 0; i < input.length; i++) {
293
- if (input[i] === " ") {
294
- 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)}`);
295
373
  }
296
- }
297
- let lastBreakIndex = -1;
298
- for (let i = 0; i < glyphs.length; i++) {
299
- const glyph = glyphs[i];
300
- const glyphWidth = glyph.xAdvance;
301
- if (glyph.char === "\n") {
302
- if (currentLine.length > 0) {
303
- lines.push({
304
- glyphs: currentLine,
305
- width: currentWidth,
306
- y: 0
307
- });
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);
308
406
  }
309
- currentLine = [];
310
- currentWidth = 0;
311
- lastBreakIndex = i;
312
- continue;
313
407
  }
314
- if (currentWidth + glyphWidth > width && currentLine.length > 0) {
315
- if (lastBreakIndex > -1) {
316
- const breakPoint = lastBreakIndex - (i - currentLine.length) + 1;
317
- const nextLine = currentLine.splice(breakPoint);
318
- const lineWidth = currentLine.reduce((sum, g) => sum + g.xAdvance, 0);
319
- lines.push({
320
- glyphs: currentLine,
321
- width: lineWidth,
322
- y: 0
323
- });
324
- currentLine = nextLine;
325
- currentWidth = nextLine.reduce((sum, g) => sum + g.xAdvance, 0);
326
- } else {
327
- lines.push({
328
- glyphs: currentLine,
329
- width: currentWidth,
330
- y: 0
331
- });
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
+ }
332
420
  currentLine = [];
333
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;
334
452
  }
335
- lastBreakIndex = -1;
336
453
  }
337
- currentLine.push(glyph);
338
- currentWidth += glyphWidth;
339
- if (spaceIndices.has(glyph.cluster)) {
340
- lastBreakIndex = i;
454
+ if (currentLine.length > 0) {
455
+ lines.push({
456
+ glyphs: currentLine,
457
+ width: currentWidth,
458
+ y: 0
459
+ });
341
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)}`);
342
471
  }
343
- if (currentLine.length > 0) {
344
- lines.push({
345
- glyphs: currentLine,
346
- width: currentWidth,
347
- y: 0
348
- });
349
- }
350
- const lineHeight = params.lineHeight * fontSize;
351
- for (let i = 0; i < lines.length; i++) {
352
- lines[i].y = (i + 1) * lineHeight;
353
- }
354
- return lines;
355
472
  }
356
473
  };
357
474
 
@@ -443,7 +560,6 @@ async function buildDrawOps(p) {
443
560
  path,
444
561
  x: glyphX + p.shadow.offsetX,
445
562
  y: glyphY + p.shadow.offsetY,
446
- // @ts-ignore scale propagated to painters
447
563
  scale,
448
564
  fill: { kind: "solid", color: p.shadow.color, opacity: p.shadow.opacity }
449
565
  });
@@ -454,7 +570,6 @@ async function buildDrawOps(p) {
454
570
  path,
455
571
  x: glyphX,
456
572
  y: glyphY,
457
- // @ts-ignore scale propagated to painters
458
573
  scale,
459
574
  width: p.stroke.width,
460
575
  color: p.stroke.color,
@@ -466,7 +581,6 @@ async function buildDrawOps(p) {
466
581
  path,
467
582
  x: glyphX,
468
583
  y: glyphY,
469
- // @ts-ignore scale propagated to painters
470
584
  scale,
471
585
  fill
472
586
  });
@@ -1042,30 +1156,32 @@ async function createNodePainter(opts) {
1042
1156
  continue;
1043
1157
  }
1044
1158
  if (op.op === "FillPath") {
1159
+ const fillOp = op;
1045
1160
  ctx.save();
1046
- ctx.translate(op.x, op.y);
1047
- const s = op.scale ?? 1;
1161
+ ctx.translate(fillOp.x, fillOp.y);
1162
+ const s = fillOp.scale ?? 1;
1048
1163
  ctx.scale(s, -s);
1049
1164
  ctx.beginPath();
1050
- drawSvgPathOnCtx(ctx, op.path);
1051
- const bbox = op.gradientBBox ?? globalBox;
1052
- 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);
1053
1168
  ctx.fillStyle = fill;
1054
1169
  ctx.fill();
1055
1170
  ctx.restore();
1056
1171
  continue;
1057
1172
  }
1058
1173
  if (op.op === "StrokePath") {
1174
+ const strokeOp = op;
1059
1175
  ctx.save();
1060
- ctx.translate(op.x, op.y);
1061
- const s = op.scale ?? 1;
1176
+ ctx.translate(strokeOp.x, strokeOp.y);
1177
+ const s = strokeOp.scale ?? 1;
1062
1178
  ctx.scale(s, -s);
1063
1179
  const invAbs = 1 / Math.abs(s);
1064
1180
  ctx.beginPath();
1065
- drawSvgPathOnCtx(ctx, op.path);
1066
- const c = parseHex6(op.color, op.opacity);
1181
+ drawSvgPathOnCtx(ctx, strokeOp.path);
1182
+ const c = parseHex6(strokeOp.color, strokeOp.opacity);
1067
1183
  ctx.strokeStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
1068
- ctx.lineWidth = op.width * invAbs;
1184
+ ctx.lineWidth = strokeOp.width * invAbs;
1069
1185
  ctx.lineJoin = "round";
1070
1186
  ctx.lineCap = "round";
1071
1187
  ctx.stroke();
@@ -1097,17 +1213,20 @@ function makeGradientFromBBox(ctx, spec, box) {
1097
1213
  const c = parseHex6(spec.color, spec.opacity);
1098
1214
  return `rgba(${c.r},${c.g},${c.b},${c.a})`;
1099
1215
  }
1100
- 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;
1101
1219
  const addStops = (g) => {
1102
- const op = spec.opacity ?? 1;
1103
- for (const s of spec.stops) {
1104
- 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);
1105
1224
  g.addColorStop(s.offset, `rgba(${c.r},${c.g},${c.b},${c.a})`);
1106
1225
  }
1107
1226
  return g;
1108
1227
  };
1109
1228
  if (spec.kind === "linear") {
1110
- const rad = (spec.angle || 0) * Math.PI / 180;
1229
+ const rad = spec.angle * Math.PI / 180;
1111
1230
  const x1 = cx + Math.cos(rad + Math.PI) * r;
1112
1231
  const y1 = cy + Math.sin(rad + Math.PI) * r;
1113
1232
  const x2 = cx + Math.cos(rad) * r;
@@ -1118,22 +1237,34 @@ function makeGradientFromBBox(ctx, spec, box) {
1118
1237
  }
1119
1238
  }
1120
1239
  function computeGlobalTextBounds(ops) {
1121
- 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;
1122
1244
  for (const op of ops) {
1123
- if (op.op !== "FillPath" || op.isShadow) continue;
1124
- const b = computePathBounds2(op.path);
1125
- const s = op.scale ?? 1;
1126
- const x1 = op.x + s * b.x;
1127
- const x2 = op.x + s * (b.x + b.w);
1128
- const y1 = op.y - s * (b.y + b.h);
1129
- 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;
1130
1254
  if (x1 < minX) minX = x1;
1131
1255
  if (y1 < minY) minY = y1;
1132
1256
  if (x2 > maxX) maxX = x2;
1133
1257
  if (y2 > maxY) maxY = y2;
1134
1258
  }
1135
- if (minX === Infinity) return { x: 0, y: 0, w: 1, h: 1 };
1136
- 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
+ };
1137
1268
  }
1138
1269
  function drawSvgPathOnCtx(ctx, d) {
1139
1270
  const t = tokenizePath2(d);
@@ -1175,13 +1306,18 @@ function drawSvgPathOnCtx(ctx, d) {
1175
1306
  ctx.closePath();
1176
1307
  break;
1177
1308
  }
1309
+ default:
1310
+ break;
1178
1311
  }
1179
1312
  }
1180
1313
  }
1181
1314
  function computePathBounds2(d) {
1182
1315
  const t = tokenizePath2(d);
1183
1316
  let i = 0;
1184
- 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;
1185
1321
  const touch = (x, y) => {
1186
1322
  if (x < minX) minX = x;
1187
1323
  if (y < minY) minY = y;
@@ -1221,10 +1357,19 @@ function computePathBounds2(d) {
1221
1357
  }
1222
1358
  case "Z":
1223
1359
  break;
1360
+ default:
1361
+ break;
1224
1362
  }
1225
1363
  }
1226
- if (minX === Infinity) return { x: 0, y: 0, w: 0, h: 0 };
1227
- 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
+ };
1228
1373
  }
1229
1374
  function roundRectPath(ctx, x, y, w, h, r) {
1230
1375
  ctx.moveTo(x + r, y);
@@ -1253,20 +1398,54 @@ function bufferToArrayBuffer(buf) {
1253
1398
  return ab.slice(byteOffset, byteOffset + byteLength);
1254
1399
  }
1255
1400
  async function loadFileOrHttpToArrayBuffer(pathOrUrl) {
1256
- if (/^https?:\/\//.test(pathOrUrl)) {
1257
- const client = pathOrUrl.startsWith("https:") ? https : http;
1258
- const buf2 = await new Promise((resolve, reject) => {
1259
- client.get(pathOrUrl, (res) => {
1260
- const chunks = [];
1261
- res.on("data", (d) => chunks.push(d));
1262
- res.on("end", () => resolve(Buffer.concat(chunks)));
1263
- res.on("error", reject);
1264
- }).on("error", reject);
1265
- });
1266
- 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)}`);
1267
1448
  }
1268
- const buf = await readFile(pathOrUrl);
1269
- return bufferToArrayBuffer(buf);
1270
1449
  }
1271
1450
 
1272
1451
  // src/core/video-generator.ts
@@ -1402,6 +1581,14 @@ var VideoGenerator = class {
1402
1581
  }
1403
1582
  };
1404
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
+
1405
1592
  // src/env/entry.node.ts
1406
1593
  async function createTextEngine(opts = {}) {
1407
1594
  const width = opts.width ?? CANVAS_CONFIG.DEFAULTS.width;
@@ -1412,125 +1599,213 @@ async function createTextEngine(opts = {}) {
1412
1599
  const fonts = new FontRegistry(wasmBaseURL);
1413
1600
  const layout = new LayoutEngine(fonts);
1414
1601
  const videoGenerator = new VideoGenerator();
1415
- 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
+ }
1416
1609
  async function ensureFonts(asset) {
1417
- if (asset.customFonts) {
1418
- for (const cf of asset.customFonts) {
1419
- const bytes = await loadFileOrHttpToArrayBuffer(cf.src);
1420
- await fonts.registerFromBytes(bytes, {
1421
- family: cf.family,
1422
- weight: cf.weight ?? "400",
1423
- style: cf.style ?? "normal"
1424
- });
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
+ }
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;
1425
1639
  }
1640
+ throw new Error(`Failed to ensure fonts: ${String(err)}`);
1426
1641
  }
1427
- const main = asset.font ?? {
1428
- family: "Roboto",
1429
- weight: "400",
1430
- style: "normal",
1431
- size: 48,
1432
- color: "#000000",
1433
- opacity: 1
1434
- };
1435
- return main;
1436
1642
  }
1437
1643
  return {
1438
1644
  validate(input) {
1439
- const { value, error } = RichTextAssetSchema.validate(input, {
1440
- abortEarly: false,
1441
- convert: true
1442
- });
1443
- if (error) throw error;
1444
- 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
+ }
1445
1658
  },
1446
1659
  async registerFontFromFile(path, desc) {
1447
- const bytes = await loadFileOrHttpToArrayBuffer(path);
1448
- 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
+ }
1449
1668
  },
1450
1669
  async registerFontFromUrl(url, desc) {
1451
- const bytes = await loadFileOrHttpToArrayBuffer(url);
1452
- 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
+ }
1453
1678
  },
1454
1679
  async renderFrame(asset, tSeconds) {
1455
- const main = await ensureFonts(asset);
1456
- const desc = { family: main.family, weight: `${main.weight}`, style: main.style };
1457
- const lines = await layout.layout({
1458
- text: asset.text,
1459
- width: asset.width ?? width,
1460
- letterSpacing: asset.style?.letterSpacing ?? 0,
1461
- fontSize: main.size,
1462
- lineHeight: asset.style?.lineHeight ?? 1.2,
1463
- desc,
1464
- textTransform: asset.style?.textTransform ?? "none"
1465
- });
1466
- const textRect = { x: 0, y: 0, width: asset.width ?? width, height: asset.height ?? height };
1467
- const canvasW = asset.width ?? width;
1468
- const canvasH = asset.height ?? height;
1469
- const canvasPR = asset.pixelRatio ?? pixelRatio;
1470
- const ops0 = await buildDrawOps({
1471
- canvas: { width: canvasW, height: canvasH, pixelRatio: canvasPR },
1472
- textRect,
1473
- lines,
1474
- font: {
1475
- family: main.family,
1476
- size: main.size,
1477
- weight: `${main.weight}`,
1478
- style: main.style,
1479
- color: main.color,
1480
- opacity: main.opacity
1481
- },
1482
- style: {
1483
- lineHeight: asset.style?.lineHeight ?? 1.2,
1484
- textDecoration: asset.style?.textDecoration ?? "none",
1485
- gradient: asset.style?.gradient
1486
- },
1487
- stroke: asset.stroke,
1488
- shadow: asset.shadow,
1489
- align: asset.align ?? { horizontal: "left", vertical: "middle" },
1490
- background: asset.background,
1491
- glyphPathProvider: (gid) => fonts.glyphPath(desc, gid),
1492
- getUnitsPerEm: () => fonts.getUnitsPerEm(desc)
1493
- });
1494
- const ops = applyAnimation(ops0, lines, {
1495
- t: tSeconds,
1496
- fontSize: main.size,
1497
- anim: asset.animation ? {
1498
- preset: asset.animation.preset,
1499
- speed: asset.animation.speed,
1500
- duration: asset.animation.duration,
1501
- style: asset.animation.style,
1502
- direction: asset.animation.direction
1503
- } : void 0
1504
- });
1505
- 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
+ }
1506
1763
  },
1507
1764
  async createRenderer(p) {
1508
- return createNodePainter({
1509
- width: p.width ?? width,
1510
- height: p.height ?? height,
1511
- pixelRatio: p.pixelRatio ?? pixelRatio
1512
- });
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
+ }
1513
1776
  },
1514
1777
  async generateVideo(asset, options) {
1515
- const finalOptions = {
1516
- width: asset.width ?? width,
1517
- height: asset.height ?? height,
1518
- fps,
1519
- duration: asset.animation?.duration ?? 3,
1520
- outputPath: options.outputPath ?? "output.mp4",
1521
- pixelRatio: asset.pixelRatio ?? pixelRatio
1522
- };
1523
- const frameGenerator = async (time) => {
1524
- return this.renderFrame(asset, time);
1525
- };
1526
- 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
+ }
1527
1796
  },
1528
1797
  destroy() {
1529
- fonts.destroy();
1798
+ try {
1799
+ fonts.destroy();
1800
+ } catch (err) {
1801
+ console.error(`Error during cleanup: ${err instanceof Error ? err.message : String(err)}`);
1802
+ }
1530
1803
  }
1531
1804
  };
1532
1805
  }
1533
1806
  export {
1534
- createTextEngine
1807
+ createTextEngine,
1808
+ isGlyphFill2 as isGlyphFill,
1809
+ isShadowFill2 as isShadowFill
1535
1810
  };
1536
1811
  //# sourceMappingURL=entry.node.js.map