@shotstack/shotstack-canvas 1.1.0 → 1.1.2

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,29 +128,42 @@ 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
147
- var FontRegistry = class {
156
+ var FontRegistry = class _FontRegistry {
148
157
  hb;
149
158
  faces = /* @__PURE__ */ new Map();
150
159
  fonts = /* @__PURE__ */ new Map();
151
160
  blobs = /* @__PURE__ */ new Map();
152
161
  wasmBaseURL;
153
162
  initPromise;
163
+ static fallbackLoader;
164
+ static setFallbackLoader(loader) {
165
+ _FontRegistry.fallbackLoader = loader;
166
+ }
154
167
  constructor(wasmBaseURL) {
155
168
  this.wasmBaseURL = wasmBaseURL;
156
169
  }
@@ -159,11 +172,15 @@ var FontRegistry = class {
159
172
  await this.initPromise;
160
173
  return;
161
174
  }
162
- if (this.hb) {
163
- return;
164
- }
175
+ if (this.hb) return;
165
176
  this.initPromise = this._doInit();
166
- await this.initPromise;
177
+ try {
178
+ await this.initPromise;
179
+ } catch (err) {
180
+ throw new Error(
181
+ `Failed to initialize FontRegistry: ${err instanceof Error ? err.message : String(err)}`
182
+ );
183
+ }
167
184
  }
168
185
  async _doInit() {
169
186
  try {
@@ -175,7 +192,13 @@ var FontRegistry = class {
175
192
  }
176
193
  async getHB() {
177
194
  if (!this.hb) {
178
- await this.init();
195
+ try {
196
+ await this.init();
197
+ } catch (err) {
198
+ throw new Error(
199
+ `Failed to get HarfBuzz instance: ${err instanceof Error ? err.message : String(err)}`
200
+ );
201
+ }
179
202
  }
180
203
  return this.hb;
181
204
  }
@@ -183,48 +206,140 @@ var FontRegistry = class {
183
206
  return `${desc.family}__${desc.weight ?? "400"}__${desc.style ?? "normal"}`;
184
207
  }
185
208
  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);
209
+ try {
210
+ if (!this.hb) await this.init();
211
+ const k = this.key(desc);
212
+ if (this.fonts.has(k)) return;
213
+ const blob = this.hb.createBlob(bytes);
214
+ const face = this.hb.createFace(blob, 0);
215
+ const font = this.hb.createFont(face);
216
+ const upem = face.upem || 1e3;
217
+ font.setScale(upem, upem);
218
+ this.blobs.set(k, blob);
219
+ this.faces.set(k, face);
220
+ this.fonts.set(k, font);
221
+ } catch (err) {
222
+ throw new Error(
223
+ `Failed to register font "${desc.family}": ${err instanceof Error ? err.message : String(err)}`
224
+ );
225
+ }
226
+ }
227
+ async tryFallbackInstall(desc) {
228
+ const loader = _FontRegistry.fallbackLoader;
229
+ if (!loader) return false;
230
+ try {
231
+ const bytes = await loader({
232
+ family: desc.family,
233
+ weight: desc.weight ?? "400",
234
+ style: desc.style ?? "normal"
235
+ });
236
+ if (!bytes) return false;
237
+ await this.registerFromBytes(bytes, {
238
+ family: desc.family,
239
+ weight: desc.weight ?? "400",
240
+ style: desc.style ?? "normal"
241
+ });
242
+ return true;
243
+ } catch {
244
+ return false;
245
+ }
197
246
  }
198
247
  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;
248
+ try {
249
+ if (!this.hb) await this.init();
250
+ const k = this.key(desc);
251
+ let f = this.fonts.get(k);
252
+ if (!f) {
253
+ const installed = await this.tryFallbackInstall(desc);
254
+ f = installed ? this.fonts.get(k) : void 0;
255
+ }
256
+ if (!f) throw new Error(`Font not registered for ${k}`);
257
+ return f;
258
+ } catch (err) {
259
+ if (err instanceof Error && err.message.includes("Font not registered")) {
260
+ throw err;
261
+ }
262
+ throw new Error(
263
+ `Failed to get font "${desc.family}": ${err instanceof Error ? err.message : String(err)}`
264
+ );
265
+ }
204
266
  }
205
267
  async getFace(desc) {
206
- if (!this.hb) await this.init();
207
- const k = this.key(desc);
208
- return this.faces.get(k);
268
+ try {
269
+ if (!this.hb) await this.init();
270
+ const k = this.key(desc);
271
+ let face = this.faces.get(k);
272
+ if (!face) {
273
+ const installed = await this.tryFallbackInstall(desc);
274
+ face = installed ? this.faces.get(k) : void 0;
275
+ }
276
+ return face;
277
+ } catch (err) {
278
+ throw new Error(
279
+ `Failed to get face for "${desc.family}": ${err instanceof Error ? err.message : String(err)}`
280
+ );
281
+ }
209
282
  }
210
283
  async getUnitsPerEm(desc) {
211
- const face = await this.getFace(desc);
212
- return face?.upem || 1e3;
284
+ try {
285
+ const face = await this.getFace(desc);
286
+ return face?.upem || 1e3;
287
+ } catch (err) {
288
+ throw new Error(
289
+ `Failed to get units per em for "${desc.family}": ${err instanceof Error ? err.message : String(err)}`
290
+ );
291
+ }
213
292
  }
214
293
  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";
294
+ try {
295
+ const font = await this.getFont(desc);
296
+ const path = font.glyphToPath(glyphId);
297
+ return path && path !== "" ? path : "M 0 0";
298
+ } catch (err) {
299
+ throw new Error(
300
+ `Failed to get glyph path for glyph ${glyphId} in font "${desc.family}": ${err instanceof Error ? err.message : String(err)}`
301
+ );
302
+ }
218
303
  }
219
304
  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;
305
+ try {
306
+ for (const [, f] of this.fonts) {
307
+ try {
308
+ f.destroy();
309
+ } catch (err) {
310
+ console.error(
311
+ `Error destroying font: ${err instanceof Error ? err.message : String(err)}`
312
+ );
313
+ }
314
+ }
315
+ for (const [, f] of this.faces) {
316
+ try {
317
+ f.destroy();
318
+ } catch (err) {
319
+ console.error(
320
+ `Error destroying face: ${err instanceof Error ? err.message : String(err)}`
321
+ );
322
+ }
323
+ }
324
+ for (const [, b] of this.blobs) {
325
+ try {
326
+ b.destroy();
327
+ } catch (err) {
328
+ console.error(
329
+ `Error destroying blob: ${err instanceof Error ? err.message : String(err)}`
330
+ );
331
+ }
332
+ }
333
+ this.fonts.clear();
334
+ this.faces.clear();
335
+ this.blobs.clear();
336
+ this.hb = void 0;
337
+ this.initPromise = void 0;
338
+ } catch (err) {
339
+ console.error(
340
+ `Error during FontRegistry cleanup: ${err instanceof Error ? err.message : String(err)}`
341
+ );
342
+ }
228
343
  }
229
344
  };
230
345
 
@@ -246,112 +361,145 @@ var LayoutEngine = class {
246
361
  }
247
362
  }
248
363
  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;
364
+ try {
365
+ const hb = await this.fonts.getHB();
366
+ const buffer = hb.createBuffer();
367
+ try {
368
+ buffer.addText(text);
369
+ buffer.guessSegmentProperties();
370
+ const font = await this.fonts.getFont(desc);
371
+ const face = await this.fonts.getFace(desc);
372
+ const upem = face?.upem || 1e3;
373
+ font.setScale(upem, upem);
374
+ hb.shape(font, buffer);
375
+ const result = buffer.json();
376
+ return result;
377
+ } finally {
378
+ try {
379
+ buffer.destroy();
380
+ } catch (err) {
381
+ console.error(
382
+ `Error destroying HarfBuzz buffer: ${err instanceof Error ? err.message : String(err)}`
383
+ );
384
+ }
385
+ }
386
+ } catch (err) {
387
+ throw new Error(
388
+ `Failed to shape text with font "${desc.family}": ${err instanceof Error ? err.message : String(err)}`
389
+ );
390
+ }
261
391
  }
262
392
  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];
393
+ try {
394
+ const { textTransform, desc, fontSize, letterSpacing, width } = params;
395
+ const input = this.transformText(params.text, textTransform);
396
+ if (!input || input.length === 0) {
397
+ return [];
277
398
  }
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);
399
+ let shaped;
400
+ try {
401
+ shaped = await this.shapeFull(input, desc);
402
+ } catch (err) {
403
+ throw new Error(`Text shaping failed: ${err instanceof Error ? err.message : String(err)}`);
295
404
  }
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
- });
405
+ let upem;
406
+ try {
407
+ const face = await this.fonts.getFace(desc);
408
+ upem = face?.upem || 1e3;
409
+ } catch (err) {
410
+ throw new Error(
411
+ `Failed to get font metrics: ${err instanceof Error ? err.message : String(err)}`
412
+ );
413
+ }
414
+ const scale = fontSize / upem;
415
+ const glyphs = shaped.map((g) => {
416
+ const charIndex = g.cl;
417
+ let char;
418
+ if (charIndex >= 0 && charIndex < input.length) {
419
+ char = input[charIndex];
420
+ }
421
+ return {
422
+ id: g.g,
423
+ xAdvance: g.ax * scale + letterSpacing,
424
+ xOffset: g.dx * scale,
425
+ yOffset: -g.dy * scale,
426
+ cluster: g.cl,
427
+ char
428
+ };
429
+ });
430
+ const lines = [];
431
+ let currentLine = [];
432
+ let currentWidth = 0;
433
+ const spaceIndices = /* @__PURE__ */ new Set();
434
+ for (let i = 0; i < input.length; i++) {
435
+ if (input[i] === " ") {
436
+ spaceIndices.add(i);
308
437
  }
309
- currentLine = [];
310
- currentWidth = 0;
311
- lastBreakIndex = i;
312
- continue;
313
438
  }
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
- });
439
+ let lastBreakIndex = -1;
440
+ for (let i = 0; i < glyphs.length; i++) {
441
+ const glyph = glyphs[i];
442
+ const glyphWidth = glyph.xAdvance;
443
+ if (glyph.char === "\n") {
444
+ if (currentLine.length > 0) {
445
+ lines.push({
446
+ glyphs: currentLine,
447
+ width: currentWidth,
448
+ y: 0
449
+ });
450
+ }
332
451
  currentLine = [];
333
452
  currentWidth = 0;
453
+ lastBreakIndex = i;
454
+ continue;
455
+ }
456
+ if (currentWidth + glyphWidth > width && currentLine.length > 0) {
457
+ if (lastBreakIndex > -1) {
458
+ const breakPoint = lastBreakIndex - (i - currentLine.length) + 1;
459
+ const nextLine = currentLine.splice(breakPoint);
460
+ const lineWidth = currentLine.reduce((sum, g) => sum + g.xAdvance, 0);
461
+ lines.push({
462
+ glyphs: currentLine,
463
+ width: lineWidth,
464
+ y: 0
465
+ });
466
+ currentLine = nextLine;
467
+ currentWidth = nextLine.reduce((sum, g) => sum + g.xAdvance, 0);
468
+ } else {
469
+ lines.push({
470
+ glyphs: currentLine,
471
+ width: currentWidth,
472
+ y: 0
473
+ });
474
+ currentLine = [];
475
+ currentWidth = 0;
476
+ }
477
+ lastBreakIndex = -1;
478
+ }
479
+ currentLine.push(glyph);
480
+ currentWidth += glyphWidth;
481
+ if (spaceIndices.has(glyph.cluster)) {
482
+ lastBreakIndex = i;
334
483
  }
335
- lastBreakIndex = -1;
336
484
  }
337
- currentLine.push(glyph);
338
- currentWidth += glyphWidth;
339
- if (spaceIndices.has(glyph.cluster)) {
340
- lastBreakIndex = i;
485
+ if (currentLine.length > 0) {
486
+ lines.push({
487
+ glyphs: currentLine,
488
+ width: currentWidth,
489
+ y: 0
490
+ });
341
491
  }
492
+ const lineHeight = params.lineHeight * fontSize;
493
+ for (let i = 0; i < lines.length; i++) {
494
+ lines[i].y = (i + 1) * lineHeight;
495
+ }
496
+ return lines;
497
+ } catch (err) {
498
+ if (err instanceof Error) {
499
+ throw err;
500
+ }
501
+ throw new Error(`Layout failed: ${String(err)}`);
342
502
  }
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
503
  }
356
504
  };
357
505
 
@@ -443,7 +591,6 @@ async function buildDrawOps(p) {
443
591
  path,
444
592
  x: glyphX + p.shadow.offsetX,
445
593
  y: glyphY + p.shadow.offsetY,
446
- // @ts-ignore scale propagated to painters
447
594
  scale,
448
595
  fill: { kind: "solid", color: p.shadow.color, opacity: p.shadow.opacity }
449
596
  });
@@ -454,7 +601,6 @@ async function buildDrawOps(p) {
454
601
  path,
455
602
  x: glyphX,
456
603
  y: glyphY,
457
- // @ts-ignore scale propagated to painters
458
604
  scale,
459
605
  width: p.stroke.width,
460
606
  color: p.stroke.color,
@@ -466,7 +612,6 @@ async function buildDrawOps(p) {
466
612
  path,
467
613
  x: glyphX,
468
614
  y: glyphY,
469
- // @ts-ignore scale propagated to painters
470
615
  scale,
471
616
  fill
472
617
  });
@@ -1042,30 +1187,32 @@ async function createNodePainter(opts) {
1042
1187
  continue;
1043
1188
  }
1044
1189
  if (op.op === "FillPath") {
1190
+ const fillOp = op;
1045
1191
  ctx.save();
1046
- ctx.translate(op.x, op.y);
1047
- const s = op.scale ?? 1;
1192
+ ctx.translate(fillOp.x, fillOp.y);
1193
+ const s = fillOp.scale ?? 1;
1048
1194
  ctx.scale(s, -s);
1049
1195
  ctx.beginPath();
1050
- drawSvgPathOnCtx(ctx, op.path);
1051
- const bbox = op.gradientBBox ?? globalBox;
1052
- const fill = makeGradientFromBBox(ctx, op.fill, bbox);
1196
+ drawSvgPathOnCtx(ctx, fillOp.path);
1197
+ const bbox = fillOp.gradientBBox ?? globalBox;
1198
+ const fill = makeGradientFromBBox(ctx, fillOp.fill, bbox);
1053
1199
  ctx.fillStyle = fill;
1054
1200
  ctx.fill();
1055
1201
  ctx.restore();
1056
1202
  continue;
1057
1203
  }
1058
1204
  if (op.op === "StrokePath") {
1205
+ const strokeOp = op;
1059
1206
  ctx.save();
1060
- ctx.translate(op.x, op.y);
1061
- const s = op.scale ?? 1;
1207
+ ctx.translate(strokeOp.x, strokeOp.y);
1208
+ const s = strokeOp.scale ?? 1;
1062
1209
  ctx.scale(s, -s);
1063
1210
  const invAbs = 1 / Math.abs(s);
1064
1211
  ctx.beginPath();
1065
- drawSvgPathOnCtx(ctx, op.path);
1066
- const c = parseHex6(op.color, op.opacity);
1212
+ drawSvgPathOnCtx(ctx, strokeOp.path);
1213
+ const c = parseHex6(strokeOp.color, strokeOp.opacity);
1067
1214
  ctx.strokeStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
1068
- ctx.lineWidth = op.width * invAbs;
1215
+ ctx.lineWidth = strokeOp.width * invAbs;
1069
1216
  ctx.lineJoin = "round";
1070
1217
  ctx.lineCap = "round";
1071
1218
  ctx.stroke();
@@ -1097,17 +1244,20 @@ function makeGradientFromBBox(ctx, spec, box) {
1097
1244
  const c = parseHex6(spec.color, spec.opacity);
1098
1245
  return `rgba(${c.r},${c.g},${c.b},${c.a})`;
1099
1246
  }
1100
- const cx = box.x + box.w / 2, cy = box.y + box.h / 2, r = Math.max(box.w, box.h) / 2;
1247
+ const cx = box.x + box.w / 2;
1248
+ const cy = box.y + box.h / 2;
1249
+ const r = Math.max(box.w, box.h) / 2;
1101
1250
  const addStops = (g) => {
1102
- const op = spec.opacity ?? 1;
1103
- for (const s of spec.stops) {
1104
- const c = parseHex6(s.color, op);
1251
+ const opacity = spec.kind === "linear" || spec.kind === "radial" ? spec.opacity : 1;
1252
+ const stops = spec.kind === "linear" || spec.kind === "radial" ? spec.stops : [];
1253
+ for (const s of stops) {
1254
+ const c = parseHex6(s.color, opacity);
1105
1255
  g.addColorStop(s.offset, `rgba(${c.r},${c.g},${c.b},${c.a})`);
1106
1256
  }
1107
1257
  return g;
1108
1258
  };
1109
1259
  if (spec.kind === "linear") {
1110
- const rad = (spec.angle || 0) * Math.PI / 180;
1260
+ const rad = spec.angle * Math.PI / 180;
1111
1261
  const x1 = cx + Math.cos(rad + Math.PI) * r;
1112
1262
  const y1 = cy + Math.sin(rad + Math.PI) * r;
1113
1263
  const x2 = cx + Math.cos(rad) * r;
@@ -1118,22 +1268,34 @@ function makeGradientFromBBox(ctx, spec, box) {
1118
1268
  }
1119
1269
  }
1120
1270
  function computeGlobalTextBounds(ops) {
1121
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1271
+ let minX = Infinity;
1272
+ let minY = Infinity;
1273
+ let maxX = -Infinity;
1274
+ let maxY = -Infinity;
1122
1275
  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;
1276
+ if (op.op !== "FillPath") continue;
1277
+ const fillOp = op;
1278
+ if (fillOp.isShadow) continue;
1279
+ const b = computePathBounds2(fillOp.path);
1280
+ const s = fillOp.scale ?? 1;
1281
+ const x1 = fillOp.x + s * b.x;
1282
+ const x2 = fillOp.x + s * (b.x + b.w);
1283
+ const y1 = fillOp.y - s * (b.y + b.h);
1284
+ const y2 = fillOp.y - s * b.y;
1130
1285
  if (x1 < minX) minX = x1;
1131
1286
  if (y1 < minY) minY = y1;
1132
1287
  if (x2 > maxX) maxX = x2;
1133
1288
  if (y2 > maxY) maxY = y2;
1134
1289
  }
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) };
1290
+ if (minX === Infinity) {
1291
+ return { x: 0, y: 0, w: 1, h: 1 };
1292
+ }
1293
+ return {
1294
+ x: minX,
1295
+ y: minY,
1296
+ w: Math.max(1, maxX - minX),
1297
+ h: Math.max(1, maxY - minY)
1298
+ };
1137
1299
  }
1138
1300
  function drawSvgPathOnCtx(ctx, d) {
1139
1301
  const t = tokenizePath2(d);
@@ -1175,13 +1337,18 @@ function drawSvgPathOnCtx(ctx, d) {
1175
1337
  ctx.closePath();
1176
1338
  break;
1177
1339
  }
1340
+ default:
1341
+ break;
1178
1342
  }
1179
1343
  }
1180
1344
  }
1181
1345
  function computePathBounds2(d) {
1182
1346
  const t = tokenizePath2(d);
1183
1347
  let i = 0;
1184
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1348
+ let minX = Infinity;
1349
+ let minY = Infinity;
1350
+ let maxX = -Infinity;
1351
+ let maxY = -Infinity;
1185
1352
  const touch = (x, y) => {
1186
1353
  if (x < minX) minX = x;
1187
1354
  if (y < minY) minY = y;
@@ -1221,10 +1388,19 @@ function computePathBounds2(d) {
1221
1388
  }
1222
1389
  case "Z":
1223
1390
  break;
1391
+ default:
1392
+ break;
1224
1393
  }
1225
1394
  }
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 };
1395
+ if (minX === Infinity) {
1396
+ return { x: 0, y: 0, w: 0, h: 0 };
1397
+ }
1398
+ return {
1399
+ x: minX,
1400
+ y: minY,
1401
+ w: maxX - minX,
1402
+ h: maxY - minY
1403
+ };
1228
1404
  }
1229
1405
  function roundRectPath(ctx, x, y, w, h, r) {
1230
1406
  ctx.moveTo(x + r, y);
@@ -1253,20 +1429,54 @@ function bufferToArrayBuffer(buf) {
1253
1429
  return ab.slice(byteOffset, byteOffset + byteLength);
1254
1430
  }
1255
1431
  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);
1432
+ try {
1433
+ if (/^https?:\/\//.test(pathOrUrl)) {
1434
+ const client = pathOrUrl.startsWith("https:") ? https : http;
1435
+ const buf2 = await new Promise((resolve, reject) => {
1436
+ const request = client.get(pathOrUrl, (res) => {
1437
+ const { statusCode } = res;
1438
+ if (statusCode && (statusCode < 200 || statusCode >= 300)) {
1439
+ reject(new Error(`HTTP request failed with status ${statusCode} for ${pathOrUrl}`));
1440
+ res.resume();
1441
+ return;
1442
+ }
1443
+ const chunks = [];
1444
+ res.on("data", (chunk) => {
1445
+ chunks.push(chunk);
1446
+ });
1447
+ res.on("end", () => {
1448
+ try {
1449
+ resolve(Buffer.concat(chunks));
1450
+ } catch (err) {
1451
+ reject(
1452
+ new Error(
1453
+ `Failed to concatenate response chunks: ${err instanceof Error ? err.message : String(err)}`
1454
+ )
1455
+ );
1456
+ }
1457
+ });
1458
+ res.on("error", (err) => {
1459
+ reject(new Error(`Response error for ${pathOrUrl}: ${err.message}`));
1460
+ });
1461
+ });
1462
+ request.on("error", (err) => {
1463
+ reject(new Error(`Request error for ${pathOrUrl}: ${err.message}`));
1464
+ });
1465
+ request.setTimeout(3e4, () => {
1466
+ request.destroy();
1467
+ reject(new Error(`Request timeout after 30s for ${pathOrUrl}`));
1468
+ });
1469
+ });
1470
+ return bufferToArrayBuffer(buf2);
1471
+ }
1472
+ const buf = await readFile(pathOrUrl);
1473
+ return bufferToArrayBuffer(buf);
1474
+ } catch (err) {
1475
+ if (err instanceof Error) {
1476
+ throw new Error(`Failed to load ${pathOrUrl}: ${err.message}`);
1477
+ }
1478
+ throw new Error(`Failed to load ${pathOrUrl}: ${String(err)}`);
1267
1479
  }
1268
- const buf = await readFile(pathOrUrl);
1269
- return bufferToArrayBuffer(buf);
1270
1480
  }
1271
1481
 
1272
1482
  // src/core/video-generator.ts
@@ -1402,6 +1612,14 @@ var VideoGenerator = class {
1402
1612
  }
1403
1613
  };
1404
1614
 
1615
+ // src/types.ts
1616
+ var isShadowFill2 = (op) => {
1617
+ return op.op === "FillPath" && op.isShadow === true;
1618
+ };
1619
+ var isGlyphFill2 = (op) => {
1620
+ return op.op === "FillPath" && op.isShadow !== true;
1621
+ };
1622
+
1405
1623
  // src/env/entry.node.ts
1406
1624
  async function createTextEngine(opts = {}) {
1407
1625
  const width = opts.width ?? CANVAS_CONFIG.DEFAULTS.width;
@@ -1412,125 +1630,213 @@ async function createTextEngine(opts = {}) {
1412
1630
  const fonts = new FontRegistry(wasmBaseURL);
1413
1631
  const layout = new LayoutEngine(fonts);
1414
1632
  const videoGenerator = new VideoGenerator();
1415
- await fonts.init();
1633
+ try {
1634
+ await fonts.init();
1635
+ } catch (err) {
1636
+ throw new Error(
1637
+ `Failed to initialize font registry: ${err instanceof Error ? err.message : String(err)}`
1638
+ );
1639
+ }
1416
1640
  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
- });
1641
+ try {
1642
+ if (asset.customFonts) {
1643
+ for (const cf of asset.customFonts) {
1644
+ try {
1645
+ const bytes = await loadFileOrHttpToArrayBuffer(cf.src);
1646
+ await fonts.registerFromBytes(bytes, {
1647
+ family: cf.family,
1648
+ weight: cf.weight ?? "400",
1649
+ style: cf.style ?? "normal"
1650
+ });
1651
+ } catch (err) {
1652
+ throw new Error(
1653
+ `Failed to load custom font "${cf.family}" from ${cf.src}: ${err instanceof Error ? err.message : String(err)}`
1654
+ );
1655
+ }
1656
+ }
1425
1657
  }
1658
+ const main = asset.font ?? {
1659
+ family: "Roboto",
1660
+ weight: "400",
1661
+ style: "normal",
1662
+ size: 48,
1663
+ color: "#000000",
1664
+ opacity: 1
1665
+ };
1666
+ return main;
1667
+ } catch (err) {
1668
+ if (err instanceof Error) {
1669
+ throw err;
1670
+ }
1671
+ throw new Error(`Failed to ensure fonts: ${String(err)}`);
1426
1672
  }
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
1673
  }
1437
1674
  return {
1438
1675
  validate(input) {
1439
- const { value, error } = RichTextAssetSchema.validate(input, {
1440
- abortEarly: false,
1441
- convert: true
1442
- });
1443
- if (error) throw error;
1444
- return { value };
1676
+ try {
1677
+ const { value, error } = RichTextAssetSchema.validate(input, {
1678
+ abortEarly: false,
1679
+ convert: true
1680
+ });
1681
+ if (error) throw error;
1682
+ return { value };
1683
+ } catch (err) {
1684
+ if (err instanceof Error) {
1685
+ throw new Error(`Validation failed: ${err.message}`);
1686
+ }
1687
+ throw new Error(`Validation failed: ${String(err)}`);
1688
+ }
1445
1689
  },
1446
1690
  async registerFontFromFile(path, desc) {
1447
- const bytes = await loadFileOrHttpToArrayBuffer(path);
1448
- await fonts.registerFromBytes(bytes, desc);
1691
+ try {
1692
+ const bytes = await loadFileOrHttpToArrayBuffer(path);
1693
+ await fonts.registerFromBytes(bytes, desc);
1694
+ } catch (err) {
1695
+ throw new Error(
1696
+ `Failed to register font "${desc.family}" from file ${path}: ${err instanceof Error ? err.message : String(err)}`
1697
+ );
1698
+ }
1449
1699
  },
1450
1700
  async registerFontFromUrl(url, desc) {
1451
- const bytes = await loadFileOrHttpToArrayBuffer(url);
1452
- await fonts.registerFromBytes(bytes, desc);
1701
+ try {
1702
+ const bytes = await loadFileOrHttpToArrayBuffer(url);
1703
+ await fonts.registerFromBytes(bytes, desc);
1704
+ } catch (err) {
1705
+ throw new Error(
1706
+ `Failed to register font "${desc.family}" from URL ${url}: ${err instanceof Error ? err.message : String(err)}`
1707
+ );
1708
+ }
1453
1709
  },
1454
1710
  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;
1711
+ try {
1712
+ const main = await ensureFonts(asset);
1713
+ const desc = { family: main.family, weight: `${main.weight}`, style: main.style };
1714
+ let lines;
1715
+ try {
1716
+ lines = await layout.layout({
1717
+ text: asset.text,
1718
+ width: asset.width ?? width,
1719
+ letterSpacing: asset.style?.letterSpacing ?? 0,
1720
+ fontSize: main.size,
1721
+ lineHeight: asset.style?.lineHeight ?? 1.2,
1722
+ desc,
1723
+ textTransform: asset.style?.textTransform ?? "none"
1724
+ });
1725
+ } catch (err) {
1726
+ throw new Error(
1727
+ `Failed to layout text: ${err instanceof Error ? err.message : String(err)}`
1728
+ );
1729
+ }
1730
+ const textRect = {
1731
+ x: 0,
1732
+ y: 0,
1733
+ width: asset.width ?? width,
1734
+ height: asset.height ?? height
1735
+ };
1736
+ const canvasW = asset.width ?? width;
1737
+ const canvasH = asset.height ?? height;
1738
+ const canvasPR = asset.pixelRatio ?? pixelRatio;
1739
+ let ops0;
1740
+ try {
1741
+ ops0 = await buildDrawOps({
1742
+ canvas: { width: canvasW, height: canvasH, pixelRatio: canvasPR },
1743
+ textRect,
1744
+ lines,
1745
+ font: {
1746
+ family: main.family,
1747
+ size: main.size,
1748
+ weight: `${main.weight}`,
1749
+ style: main.style,
1750
+ color: main.color,
1751
+ opacity: main.opacity
1752
+ },
1753
+ style: {
1754
+ lineHeight: asset.style?.lineHeight ?? 1.2,
1755
+ textDecoration: asset.style?.textDecoration ?? "none",
1756
+ gradient: asset.style?.gradient
1757
+ },
1758
+ stroke: asset.stroke,
1759
+ shadow: asset.shadow,
1760
+ align: asset.align ?? { horizontal: "left", vertical: "middle" },
1761
+ background: asset.background,
1762
+ glyphPathProvider: (gid) => fonts.glyphPath(desc, gid),
1763
+ getUnitsPerEm: () => fonts.getUnitsPerEm(desc)
1764
+ });
1765
+ } catch (err) {
1766
+ throw new Error(
1767
+ `Failed to build draw operations: ${err instanceof Error ? err.message : String(err)}`
1768
+ );
1769
+ }
1770
+ try {
1771
+ const ops = applyAnimation(ops0, lines, {
1772
+ t: tSeconds,
1773
+ fontSize: main.size,
1774
+ anim: asset.animation ? {
1775
+ preset: asset.animation.preset,
1776
+ speed: asset.animation.speed,
1777
+ duration: asset.animation.duration,
1778
+ style: asset.animation.style,
1779
+ direction: asset.animation.direction
1780
+ } : void 0
1781
+ });
1782
+ return ops;
1783
+ } catch (err) {
1784
+ throw new Error(
1785
+ `Failed to apply animation: ${err instanceof Error ? err.message : String(err)}`
1786
+ );
1787
+ }
1788
+ } catch (err) {
1789
+ if (err instanceof Error) {
1790
+ throw err;
1791
+ }
1792
+ throw new Error(`Failed to render frame at time ${tSeconds}s: ${String(err)}`);
1793
+ }
1506
1794
  },
1507
1795
  async createRenderer(p) {
1508
- return createNodePainter({
1509
- width: p.width ?? width,
1510
- height: p.height ?? height,
1511
- pixelRatio: p.pixelRatio ?? pixelRatio
1512
- });
1796
+ try {
1797
+ return await createNodePainter({
1798
+ width: p.width ?? width,
1799
+ height: p.height ?? height,
1800
+ pixelRatio: p.pixelRatio ?? pixelRatio
1801
+ });
1802
+ } catch (err) {
1803
+ throw new Error(
1804
+ `Failed to create renderer: ${err instanceof Error ? err.message : String(err)}`
1805
+ );
1806
+ }
1513
1807
  },
1514
1808
  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);
1809
+ try {
1810
+ const finalOptions = {
1811
+ width: asset.width ?? width,
1812
+ height: asset.height ?? height,
1813
+ fps,
1814
+ duration: asset.animation?.duration ?? 3,
1815
+ outputPath: options.outputPath ?? "output.mp4",
1816
+ pixelRatio: asset.pixelRatio ?? pixelRatio
1817
+ };
1818
+ const frameGenerator = async (time) => {
1819
+ return this.renderFrame(asset, time);
1820
+ };
1821
+ await videoGenerator.generateVideo(frameGenerator, finalOptions);
1822
+ } catch (err) {
1823
+ throw new Error(
1824
+ `Failed to generate video: ${err instanceof Error ? err.message : String(err)}`
1825
+ );
1826
+ }
1527
1827
  },
1528
1828
  destroy() {
1529
- fonts.destroy();
1829
+ try {
1830
+ fonts.destroy();
1831
+ } catch (err) {
1832
+ console.error(`Error during cleanup: ${err instanceof Error ? err.message : String(err)}`);
1833
+ }
1530
1834
  }
1531
1835
  };
1532
1836
  }
1533
1837
  export {
1534
- createTextEngine
1838
+ createTextEngine,
1839
+ isGlyphFill2 as isGlyphFill,
1840
+ isShadowFill2 as isShadowFill
1535
1841
  };
1536
1842
  //# sourceMappingURL=entry.node.js.map