@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.
@@ -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,29 +166,42 @@ 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
183
- var FontRegistry = class {
194
+ var FontRegistry = class _FontRegistry {
184
195
  hb;
185
196
  faces = /* @__PURE__ */ new Map();
186
197
  fonts = /* @__PURE__ */ new Map();
187
198
  blobs = /* @__PURE__ */ new Map();
188
199
  wasmBaseURL;
189
200
  initPromise;
201
+ static fallbackLoader;
202
+ static setFallbackLoader(loader) {
203
+ _FontRegistry.fallbackLoader = loader;
204
+ }
190
205
  constructor(wasmBaseURL) {
191
206
  this.wasmBaseURL = wasmBaseURL;
192
207
  }
@@ -195,11 +210,15 @@ var FontRegistry = class {
195
210
  await this.initPromise;
196
211
  return;
197
212
  }
198
- if (this.hb) {
199
- return;
200
- }
213
+ if (this.hb) return;
201
214
  this.initPromise = this._doInit();
202
- await this.initPromise;
215
+ try {
216
+ await this.initPromise;
217
+ } catch (err) {
218
+ throw new Error(
219
+ `Failed to initialize FontRegistry: ${err instanceof Error ? err.message : String(err)}`
220
+ );
221
+ }
203
222
  }
204
223
  async _doInit() {
205
224
  try {
@@ -211,7 +230,13 @@ var FontRegistry = class {
211
230
  }
212
231
  async getHB() {
213
232
  if (!this.hb) {
214
- await this.init();
233
+ try {
234
+ await this.init();
235
+ } catch (err) {
236
+ throw new Error(
237
+ `Failed to get HarfBuzz instance: ${err instanceof Error ? err.message : String(err)}`
238
+ );
239
+ }
215
240
  }
216
241
  return this.hb;
217
242
  }
@@ -219,48 +244,140 @@ var FontRegistry = class {
219
244
  return `${desc.family}__${desc.weight ?? "400"}__${desc.style ?? "normal"}`;
220
245
  }
221
246
  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);
247
+ try {
248
+ if (!this.hb) await this.init();
249
+ const k = this.key(desc);
250
+ if (this.fonts.has(k)) return;
251
+ const blob = this.hb.createBlob(bytes);
252
+ const face = this.hb.createFace(blob, 0);
253
+ const font = this.hb.createFont(face);
254
+ const upem = face.upem || 1e3;
255
+ font.setScale(upem, upem);
256
+ this.blobs.set(k, blob);
257
+ this.faces.set(k, face);
258
+ this.fonts.set(k, font);
259
+ } catch (err) {
260
+ throw new Error(
261
+ `Failed to register font "${desc.family}": ${err instanceof Error ? err.message : String(err)}`
262
+ );
263
+ }
264
+ }
265
+ async tryFallbackInstall(desc) {
266
+ const loader = _FontRegistry.fallbackLoader;
267
+ if (!loader) return false;
268
+ try {
269
+ const bytes = await loader({
270
+ family: desc.family,
271
+ weight: desc.weight ?? "400",
272
+ style: desc.style ?? "normal"
273
+ });
274
+ if (!bytes) return false;
275
+ await this.registerFromBytes(bytes, {
276
+ family: desc.family,
277
+ weight: desc.weight ?? "400",
278
+ style: desc.style ?? "normal"
279
+ });
280
+ return true;
281
+ } catch {
282
+ return false;
283
+ }
233
284
  }
234
285
  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;
286
+ try {
287
+ if (!this.hb) await this.init();
288
+ const k = this.key(desc);
289
+ let f = this.fonts.get(k);
290
+ if (!f) {
291
+ const installed = await this.tryFallbackInstall(desc);
292
+ f = installed ? this.fonts.get(k) : void 0;
293
+ }
294
+ if (!f) throw new Error(`Font not registered for ${k}`);
295
+ return f;
296
+ } catch (err) {
297
+ if (err instanceof Error && err.message.includes("Font not registered")) {
298
+ throw err;
299
+ }
300
+ throw new Error(
301
+ `Failed to get font "${desc.family}": ${err instanceof Error ? err.message : String(err)}`
302
+ );
303
+ }
240
304
  }
241
305
  async getFace(desc) {
242
- if (!this.hb) await this.init();
243
- const k = this.key(desc);
244
- return this.faces.get(k);
306
+ try {
307
+ if (!this.hb) await this.init();
308
+ const k = this.key(desc);
309
+ let face = this.faces.get(k);
310
+ if (!face) {
311
+ const installed = await this.tryFallbackInstall(desc);
312
+ face = installed ? this.faces.get(k) : void 0;
313
+ }
314
+ return face;
315
+ } catch (err) {
316
+ throw new Error(
317
+ `Failed to get face for "${desc.family}": ${err instanceof Error ? err.message : String(err)}`
318
+ );
319
+ }
245
320
  }
246
321
  async getUnitsPerEm(desc) {
247
- const face = await this.getFace(desc);
248
- return face?.upem || 1e3;
322
+ try {
323
+ const face = await this.getFace(desc);
324
+ return face?.upem || 1e3;
325
+ } catch (err) {
326
+ throw new Error(
327
+ `Failed to get units per em for "${desc.family}": ${err instanceof Error ? err.message : String(err)}`
328
+ );
329
+ }
249
330
  }
250
331
  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";
332
+ try {
333
+ const font = await this.getFont(desc);
334
+ const path = font.glyphToPath(glyphId);
335
+ return path && path !== "" ? path : "M 0 0";
336
+ } catch (err) {
337
+ throw new Error(
338
+ `Failed to get glyph path for glyph ${glyphId} in font "${desc.family}": ${err instanceof Error ? err.message : String(err)}`
339
+ );
340
+ }
254
341
  }
255
342
  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;
343
+ try {
344
+ for (const [, f] of this.fonts) {
345
+ try {
346
+ f.destroy();
347
+ } catch (err) {
348
+ console.error(
349
+ `Error destroying font: ${err instanceof Error ? err.message : String(err)}`
350
+ );
351
+ }
352
+ }
353
+ for (const [, f] of this.faces) {
354
+ try {
355
+ f.destroy();
356
+ } catch (err) {
357
+ console.error(
358
+ `Error destroying face: ${err instanceof Error ? err.message : String(err)}`
359
+ );
360
+ }
361
+ }
362
+ for (const [, b] of this.blobs) {
363
+ try {
364
+ b.destroy();
365
+ } catch (err) {
366
+ console.error(
367
+ `Error destroying blob: ${err instanceof Error ? err.message : String(err)}`
368
+ );
369
+ }
370
+ }
371
+ this.fonts.clear();
372
+ this.faces.clear();
373
+ this.blobs.clear();
374
+ this.hb = void 0;
375
+ this.initPromise = void 0;
376
+ } catch (err) {
377
+ console.error(
378
+ `Error during FontRegistry cleanup: ${err instanceof Error ? err.message : String(err)}`
379
+ );
380
+ }
264
381
  }
265
382
  };
266
383
 
@@ -282,112 +399,145 @@ var LayoutEngine = class {
282
399
  }
283
400
  }
284
401
  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;
402
+ try {
403
+ const hb = await this.fonts.getHB();
404
+ const buffer = hb.createBuffer();
405
+ try {
406
+ buffer.addText(text);
407
+ buffer.guessSegmentProperties();
408
+ const font = await this.fonts.getFont(desc);
409
+ const face = await this.fonts.getFace(desc);
410
+ const upem = face?.upem || 1e3;
411
+ font.setScale(upem, upem);
412
+ hb.shape(font, buffer);
413
+ const result = buffer.json();
414
+ return result;
415
+ } finally {
416
+ try {
417
+ buffer.destroy();
418
+ } catch (err) {
419
+ console.error(
420
+ `Error destroying HarfBuzz buffer: ${err instanceof Error ? err.message : String(err)}`
421
+ );
422
+ }
423
+ }
424
+ } catch (err) {
425
+ throw new Error(
426
+ `Failed to shape text with font "${desc.family}": ${err instanceof Error ? err.message : String(err)}`
427
+ );
428
+ }
297
429
  }
298
430
  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];
431
+ try {
432
+ const { textTransform, desc, fontSize, letterSpacing, width } = params;
433
+ const input = this.transformText(params.text, textTransform);
434
+ if (!input || input.length === 0) {
435
+ return [];
313
436
  }
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);
437
+ let shaped;
438
+ try {
439
+ shaped = await this.shapeFull(input, desc);
440
+ } catch (err) {
441
+ throw new Error(`Text shaping failed: ${err instanceof Error ? err.message : String(err)}`);
331
442
  }
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
- });
443
+ let upem;
444
+ try {
445
+ const face = await this.fonts.getFace(desc);
446
+ upem = face?.upem || 1e3;
447
+ } catch (err) {
448
+ throw new Error(
449
+ `Failed to get font metrics: ${err instanceof Error ? err.message : String(err)}`
450
+ );
451
+ }
452
+ const scale = fontSize / upem;
453
+ const glyphs = shaped.map((g) => {
454
+ const charIndex = g.cl;
455
+ let char;
456
+ if (charIndex >= 0 && charIndex < input.length) {
457
+ char = input[charIndex];
458
+ }
459
+ return {
460
+ id: g.g,
461
+ xAdvance: g.ax * scale + letterSpacing,
462
+ xOffset: g.dx * scale,
463
+ yOffset: -g.dy * scale,
464
+ cluster: g.cl,
465
+ char
466
+ };
467
+ });
468
+ const lines = [];
469
+ let currentLine = [];
470
+ let currentWidth = 0;
471
+ const spaceIndices = /* @__PURE__ */ new Set();
472
+ for (let i = 0; i < input.length; i++) {
473
+ if (input[i] === " ") {
474
+ spaceIndices.add(i);
344
475
  }
345
- currentLine = [];
346
- currentWidth = 0;
347
- lastBreakIndex = i;
348
- continue;
349
476
  }
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
- });
477
+ let lastBreakIndex = -1;
478
+ for (let i = 0; i < glyphs.length; i++) {
479
+ const glyph = glyphs[i];
480
+ const glyphWidth = glyph.xAdvance;
481
+ if (glyph.char === "\n") {
482
+ if (currentLine.length > 0) {
483
+ lines.push({
484
+ glyphs: currentLine,
485
+ width: currentWidth,
486
+ y: 0
487
+ });
488
+ }
368
489
  currentLine = [];
369
490
  currentWidth = 0;
491
+ lastBreakIndex = i;
492
+ continue;
493
+ }
494
+ if (currentWidth + glyphWidth > width && currentLine.length > 0) {
495
+ if (lastBreakIndex > -1) {
496
+ const breakPoint = lastBreakIndex - (i - currentLine.length) + 1;
497
+ const nextLine = currentLine.splice(breakPoint);
498
+ const lineWidth = currentLine.reduce((sum, g) => sum + g.xAdvance, 0);
499
+ lines.push({
500
+ glyphs: currentLine,
501
+ width: lineWidth,
502
+ y: 0
503
+ });
504
+ currentLine = nextLine;
505
+ currentWidth = nextLine.reduce((sum, g) => sum + g.xAdvance, 0);
506
+ } else {
507
+ lines.push({
508
+ glyphs: currentLine,
509
+ width: currentWidth,
510
+ y: 0
511
+ });
512
+ currentLine = [];
513
+ currentWidth = 0;
514
+ }
515
+ lastBreakIndex = -1;
516
+ }
517
+ currentLine.push(glyph);
518
+ currentWidth += glyphWidth;
519
+ if (spaceIndices.has(glyph.cluster)) {
520
+ lastBreakIndex = i;
370
521
  }
371
- lastBreakIndex = -1;
372
522
  }
373
- currentLine.push(glyph);
374
- currentWidth += glyphWidth;
375
- if (spaceIndices.has(glyph.cluster)) {
376
- lastBreakIndex = i;
523
+ if (currentLine.length > 0) {
524
+ lines.push({
525
+ glyphs: currentLine,
526
+ width: currentWidth,
527
+ y: 0
528
+ });
377
529
  }
530
+ const lineHeight = params.lineHeight * fontSize;
531
+ for (let i = 0; i < lines.length; i++) {
532
+ lines[i].y = (i + 1) * lineHeight;
533
+ }
534
+ return lines;
535
+ } catch (err) {
536
+ if (err instanceof Error) {
537
+ throw err;
538
+ }
539
+ throw new Error(`Layout failed: ${String(err)}`);
378
540
  }
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
541
  }
392
542
  };
393
543
 
@@ -479,7 +629,6 @@ async function buildDrawOps(p) {
479
629
  path,
480
630
  x: glyphX + p.shadow.offsetX,
481
631
  y: glyphY + p.shadow.offsetY,
482
- // @ts-ignore scale propagated to painters
483
632
  scale,
484
633
  fill: { kind: "solid", color: p.shadow.color, opacity: p.shadow.opacity }
485
634
  });
@@ -490,7 +639,6 @@ async function buildDrawOps(p) {
490
639
  path,
491
640
  x: glyphX,
492
641
  y: glyphY,
493
- // @ts-ignore scale propagated to painters
494
642
  scale,
495
643
  width: p.stroke.width,
496
644
  color: p.stroke.color,
@@ -502,7 +650,6 @@ async function buildDrawOps(p) {
502
650
  path,
503
651
  x: glyphX,
504
652
  y: glyphY,
505
- // @ts-ignore scale propagated to painters
506
653
  scale,
507
654
  fill
508
655
  });
@@ -1078,30 +1225,32 @@ async function createNodePainter(opts) {
1078
1225
  continue;
1079
1226
  }
1080
1227
  if (op.op === "FillPath") {
1228
+ const fillOp = op;
1081
1229
  ctx.save();
1082
- ctx.translate(op.x, op.y);
1083
- const s = op.scale ?? 1;
1230
+ ctx.translate(fillOp.x, fillOp.y);
1231
+ const s = fillOp.scale ?? 1;
1084
1232
  ctx.scale(s, -s);
1085
1233
  ctx.beginPath();
1086
- drawSvgPathOnCtx(ctx, op.path);
1087
- const bbox = op.gradientBBox ?? globalBox;
1088
- const fill = makeGradientFromBBox(ctx, op.fill, bbox);
1234
+ drawSvgPathOnCtx(ctx, fillOp.path);
1235
+ const bbox = fillOp.gradientBBox ?? globalBox;
1236
+ const fill = makeGradientFromBBox(ctx, fillOp.fill, bbox);
1089
1237
  ctx.fillStyle = fill;
1090
1238
  ctx.fill();
1091
1239
  ctx.restore();
1092
1240
  continue;
1093
1241
  }
1094
1242
  if (op.op === "StrokePath") {
1243
+ const strokeOp = op;
1095
1244
  ctx.save();
1096
- ctx.translate(op.x, op.y);
1097
- const s = op.scale ?? 1;
1245
+ ctx.translate(strokeOp.x, strokeOp.y);
1246
+ const s = strokeOp.scale ?? 1;
1098
1247
  ctx.scale(s, -s);
1099
1248
  const invAbs = 1 / Math.abs(s);
1100
1249
  ctx.beginPath();
1101
- drawSvgPathOnCtx(ctx, op.path);
1102
- const c = parseHex6(op.color, op.opacity);
1250
+ drawSvgPathOnCtx(ctx, strokeOp.path);
1251
+ const c = parseHex6(strokeOp.color, strokeOp.opacity);
1103
1252
  ctx.strokeStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
1104
- ctx.lineWidth = op.width * invAbs;
1253
+ ctx.lineWidth = strokeOp.width * invAbs;
1105
1254
  ctx.lineJoin = "round";
1106
1255
  ctx.lineCap = "round";
1107
1256
  ctx.stroke();
@@ -1133,17 +1282,20 @@ function makeGradientFromBBox(ctx, spec, box) {
1133
1282
  const c = parseHex6(spec.color, spec.opacity);
1134
1283
  return `rgba(${c.r},${c.g},${c.b},${c.a})`;
1135
1284
  }
1136
- const cx = box.x + box.w / 2, cy = box.y + box.h / 2, r = Math.max(box.w, box.h) / 2;
1285
+ const cx = box.x + box.w / 2;
1286
+ const cy = box.y + box.h / 2;
1287
+ const r = Math.max(box.w, box.h) / 2;
1137
1288
  const addStops = (g) => {
1138
- const op = spec.opacity ?? 1;
1139
- for (const s of spec.stops) {
1140
- const c = parseHex6(s.color, op);
1289
+ const opacity = spec.kind === "linear" || spec.kind === "radial" ? spec.opacity : 1;
1290
+ const stops = spec.kind === "linear" || spec.kind === "radial" ? spec.stops : [];
1291
+ for (const s of stops) {
1292
+ const c = parseHex6(s.color, opacity);
1141
1293
  g.addColorStop(s.offset, `rgba(${c.r},${c.g},${c.b},${c.a})`);
1142
1294
  }
1143
1295
  return g;
1144
1296
  };
1145
1297
  if (spec.kind === "linear") {
1146
- const rad = (spec.angle || 0) * Math.PI / 180;
1298
+ const rad = spec.angle * Math.PI / 180;
1147
1299
  const x1 = cx + Math.cos(rad + Math.PI) * r;
1148
1300
  const y1 = cy + Math.sin(rad + Math.PI) * r;
1149
1301
  const x2 = cx + Math.cos(rad) * r;
@@ -1154,22 +1306,34 @@ function makeGradientFromBBox(ctx, spec, box) {
1154
1306
  }
1155
1307
  }
1156
1308
  function computeGlobalTextBounds(ops) {
1157
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1309
+ let minX = Infinity;
1310
+ let minY = Infinity;
1311
+ let maxX = -Infinity;
1312
+ let maxY = -Infinity;
1158
1313
  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;
1314
+ if (op.op !== "FillPath") continue;
1315
+ const fillOp = op;
1316
+ if (fillOp.isShadow) continue;
1317
+ const b = computePathBounds2(fillOp.path);
1318
+ const s = fillOp.scale ?? 1;
1319
+ const x1 = fillOp.x + s * b.x;
1320
+ const x2 = fillOp.x + s * (b.x + b.w);
1321
+ const y1 = fillOp.y - s * (b.y + b.h);
1322
+ const y2 = fillOp.y - s * b.y;
1166
1323
  if (x1 < minX) minX = x1;
1167
1324
  if (y1 < minY) minY = y1;
1168
1325
  if (x2 > maxX) maxX = x2;
1169
1326
  if (y2 > maxY) maxY = y2;
1170
1327
  }
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) };
1328
+ if (minX === Infinity) {
1329
+ return { x: 0, y: 0, w: 1, h: 1 };
1330
+ }
1331
+ return {
1332
+ x: minX,
1333
+ y: minY,
1334
+ w: Math.max(1, maxX - minX),
1335
+ h: Math.max(1, maxY - minY)
1336
+ };
1173
1337
  }
1174
1338
  function drawSvgPathOnCtx(ctx, d) {
1175
1339
  const t = tokenizePath2(d);
@@ -1211,13 +1375,18 @@ function drawSvgPathOnCtx(ctx, d) {
1211
1375
  ctx.closePath();
1212
1376
  break;
1213
1377
  }
1378
+ default:
1379
+ break;
1214
1380
  }
1215
1381
  }
1216
1382
  }
1217
1383
  function computePathBounds2(d) {
1218
1384
  const t = tokenizePath2(d);
1219
1385
  let i = 0;
1220
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1386
+ let minX = Infinity;
1387
+ let minY = Infinity;
1388
+ let maxX = -Infinity;
1389
+ let maxY = -Infinity;
1221
1390
  const touch = (x, y) => {
1222
1391
  if (x < minX) minX = x;
1223
1392
  if (y < minY) minY = y;
@@ -1257,10 +1426,19 @@ function computePathBounds2(d) {
1257
1426
  }
1258
1427
  case "Z":
1259
1428
  break;
1429
+ default:
1430
+ break;
1260
1431
  }
1261
1432
  }
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 };
1433
+ if (minX === Infinity) {
1434
+ return { x: 0, y: 0, w: 0, h: 0 };
1435
+ }
1436
+ return {
1437
+ x: minX,
1438
+ y: minY,
1439
+ w: maxX - minX,
1440
+ h: maxY - minY
1441
+ };
1264
1442
  }
1265
1443
  function roundRectPath(ctx, x, y, w, h, r) {
1266
1444
  ctx.moveTo(x + r, y);
@@ -1289,20 +1467,54 @@ function bufferToArrayBuffer(buf) {
1289
1467
  return ab.slice(byteOffset, byteOffset + byteLength);
1290
1468
  }
1291
1469
  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);
1470
+ try {
1471
+ if (/^https?:\/\//.test(pathOrUrl)) {
1472
+ const client = pathOrUrl.startsWith("https:") ? https : http;
1473
+ const buf2 = await new Promise((resolve, reject) => {
1474
+ const request = client.get(pathOrUrl, (res) => {
1475
+ const { statusCode } = res;
1476
+ if (statusCode && (statusCode < 200 || statusCode >= 300)) {
1477
+ reject(new Error(`HTTP request failed with status ${statusCode} for ${pathOrUrl}`));
1478
+ res.resume();
1479
+ return;
1480
+ }
1481
+ const chunks = [];
1482
+ res.on("data", (chunk) => {
1483
+ chunks.push(chunk);
1484
+ });
1485
+ res.on("end", () => {
1486
+ try {
1487
+ resolve(Buffer.concat(chunks));
1488
+ } catch (err) {
1489
+ reject(
1490
+ new Error(
1491
+ `Failed to concatenate response chunks: ${err instanceof Error ? err.message : String(err)}`
1492
+ )
1493
+ );
1494
+ }
1495
+ });
1496
+ res.on("error", (err) => {
1497
+ reject(new Error(`Response error for ${pathOrUrl}: ${err.message}`));
1498
+ });
1499
+ });
1500
+ request.on("error", (err) => {
1501
+ reject(new Error(`Request error for ${pathOrUrl}: ${err.message}`));
1502
+ });
1503
+ request.setTimeout(3e4, () => {
1504
+ request.destroy();
1505
+ reject(new Error(`Request timeout after 30s for ${pathOrUrl}`));
1506
+ });
1507
+ });
1508
+ return bufferToArrayBuffer(buf2);
1509
+ }
1510
+ const buf = await (0, import_promises.readFile)(pathOrUrl);
1511
+ return bufferToArrayBuffer(buf);
1512
+ } catch (err) {
1513
+ if (err instanceof Error) {
1514
+ throw new Error(`Failed to load ${pathOrUrl}: ${err.message}`);
1515
+ }
1516
+ throw new Error(`Failed to load ${pathOrUrl}: ${String(err)}`);
1303
1517
  }
1304
- const buf = await (0, import_promises.readFile)(pathOrUrl);
1305
- return bufferToArrayBuffer(buf);
1306
1518
  }
1307
1519
 
1308
1520
  // src/core/video-generator.ts
@@ -1438,6 +1650,14 @@ var VideoGenerator = class {
1438
1650
  }
1439
1651
  };
1440
1652
 
1653
+ // src/types.ts
1654
+ var isShadowFill2 = (op) => {
1655
+ return op.op === "FillPath" && op.isShadow === true;
1656
+ };
1657
+ var isGlyphFill2 = (op) => {
1658
+ return op.op === "FillPath" && op.isShadow !== true;
1659
+ };
1660
+
1441
1661
  // src/env/entry.node.ts
1442
1662
  async function createTextEngine(opts = {}) {
1443
1663
  const width = opts.width ?? CANVAS_CONFIG.DEFAULTS.width;
@@ -1448,126 +1668,214 @@ async function createTextEngine(opts = {}) {
1448
1668
  const fonts = new FontRegistry(wasmBaseURL);
1449
1669
  const layout = new LayoutEngine(fonts);
1450
1670
  const videoGenerator = new VideoGenerator();
1451
- await fonts.init();
1671
+ try {
1672
+ await fonts.init();
1673
+ } catch (err) {
1674
+ throw new Error(
1675
+ `Failed to initialize font registry: ${err instanceof Error ? err.message : String(err)}`
1676
+ );
1677
+ }
1452
1678
  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
- });
1679
+ try {
1680
+ if (asset.customFonts) {
1681
+ for (const cf of asset.customFonts) {
1682
+ try {
1683
+ const bytes = await loadFileOrHttpToArrayBuffer(cf.src);
1684
+ await fonts.registerFromBytes(bytes, {
1685
+ family: cf.family,
1686
+ weight: cf.weight ?? "400",
1687
+ style: cf.style ?? "normal"
1688
+ });
1689
+ } catch (err) {
1690
+ throw new Error(
1691
+ `Failed to load custom font "${cf.family}" from ${cf.src}: ${err instanceof Error ? err.message : String(err)}`
1692
+ );
1693
+ }
1694
+ }
1461
1695
  }
1696
+ const main = asset.font ?? {
1697
+ family: "Roboto",
1698
+ weight: "400",
1699
+ style: "normal",
1700
+ size: 48,
1701
+ color: "#000000",
1702
+ opacity: 1
1703
+ };
1704
+ return main;
1705
+ } catch (err) {
1706
+ if (err instanceof Error) {
1707
+ throw err;
1708
+ }
1709
+ throw new Error(`Failed to ensure fonts: ${String(err)}`);
1462
1710
  }
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
1711
  }
1473
1712
  return {
1474
1713
  validate(input) {
1475
- const { value, error } = RichTextAssetSchema.validate(input, {
1476
- abortEarly: false,
1477
- convert: true
1478
- });
1479
- if (error) throw error;
1480
- return { value };
1714
+ try {
1715
+ const { value, error } = RichTextAssetSchema.validate(input, {
1716
+ abortEarly: false,
1717
+ convert: true
1718
+ });
1719
+ if (error) throw error;
1720
+ return { value };
1721
+ } catch (err) {
1722
+ if (err instanceof Error) {
1723
+ throw new Error(`Validation failed: ${err.message}`);
1724
+ }
1725
+ throw new Error(`Validation failed: ${String(err)}`);
1726
+ }
1481
1727
  },
1482
1728
  async registerFontFromFile(path, desc) {
1483
- const bytes = await loadFileOrHttpToArrayBuffer(path);
1484
- await fonts.registerFromBytes(bytes, desc);
1729
+ try {
1730
+ const bytes = await loadFileOrHttpToArrayBuffer(path);
1731
+ await fonts.registerFromBytes(bytes, desc);
1732
+ } catch (err) {
1733
+ throw new Error(
1734
+ `Failed to register font "${desc.family}" from file ${path}: ${err instanceof Error ? err.message : String(err)}`
1735
+ );
1736
+ }
1485
1737
  },
1486
1738
  async registerFontFromUrl(url, desc) {
1487
- const bytes = await loadFileOrHttpToArrayBuffer(url);
1488
- await fonts.registerFromBytes(bytes, desc);
1739
+ try {
1740
+ const bytes = await loadFileOrHttpToArrayBuffer(url);
1741
+ await fonts.registerFromBytes(bytes, desc);
1742
+ } catch (err) {
1743
+ throw new Error(
1744
+ `Failed to register font "${desc.family}" from URL ${url}: ${err instanceof Error ? err.message : String(err)}`
1745
+ );
1746
+ }
1489
1747
  },
1490
1748
  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;
1749
+ try {
1750
+ const main = await ensureFonts(asset);
1751
+ const desc = { family: main.family, weight: `${main.weight}`, style: main.style };
1752
+ let lines;
1753
+ try {
1754
+ lines = await layout.layout({
1755
+ text: asset.text,
1756
+ width: asset.width ?? width,
1757
+ letterSpacing: asset.style?.letterSpacing ?? 0,
1758
+ fontSize: main.size,
1759
+ lineHeight: asset.style?.lineHeight ?? 1.2,
1760
+ desc,
1761
+ textTransform: asset.style?.textTransform ?? "none"
1762
+ });
1763
+ } catch (err) {
1764
+ throw new Error(
1765
+ `Failed to layout text: ${err instanceof Error ? err.message : String(err)}`
1766
+ );
1767
+ }
1768
+ const textRect = {
1769
+ x: 0,
1770
+ y: 0,
1771
+ width: asset.width ?? width,
1772
+ height: asset.height ?? height
1773
+ };
1774
+ const canvasW = asset.width ?? width;
1775
+ const canvasH = asset.height ?? height;
1776
+ const canvasPR = asset.pixelRatio ?? pixelRatio;
1777
+ let ops0;
1778
+ try {
1779
+ ops0 = await buildDrawOps({
1780
+ canvas: { width: canvasW, height: canvasH, pixelRatio: canvasPR },
1781
+ textRect,
1782
+ lines,
1783
+ font: {
1784
+ family: main.family,
1785
+ size: main.size,
1786
+ weight: `${main.weight}`,
1787
+ style: main.style,
1788
+ color: main.color,
1789
+ opacity: main.opacity
1790
+ },
1791
+ style: {
1792
+ lineHeight: asset.style?.lineHeight ?? 1.2,
1793
+ textDecoration: asset.style?.textDecoration ?? "none",
1794
+ gradient: asset.style?.gradient
1795
+ },
1796
+ stroke: asset.stroke,
1797
+ shadow: asset.shadow,
1798
+ align: asset.align ?? { horizontal: "left", vertical: "middle" },
1799
+ background: asset.background,
1800
+ glyphPathProvider: (gid) => fonts.glyphPath(desc, gid),
1801
+ getUnitsPerEm: () => fonts.getUnitsPerEm(desc)
1802
+ });
1803
+ } catch (err) {
1804
+ throw new Error(
1805
+ `Failed to build draw operations: ${err instanceof Error ? err.message : String(err)}`
1806
+ );
1807
+ }
1808
+ try {
1809
+ const ops = applyAnimation(ops0, lines, {
1810
+ t: tSeconds,
1811
+ fontSize: main.size,
1812
+ anim: asset.animation ? {
1813
+ preset: asset.animation.preset,
1814
+ speed: asset.animation.speed,
1815
+ duration: asset.animation.duration,
1816
+ style: asset.animation.style,
1817
+ direction: asset.animation.direction
1818
+ } : void 0
1819
+ });
1820
+ return ops;
1821
+ } catch (err) {
1822
+ throw new Error(
1823
+ `Failed to apply animation: ${err instanceof Error ? err.message : String(err)}`
1824
+ );
1825
+ }
1826
+ } catch (err) {
1827
+ if (err instanceof Error) {
1828
+ throw err;
1829
+ }
1830
+ throw new Error(`Failed to render frame at time ${tSeconds}s: ${String(err)}`);
1831
+ }
1542
1832
  },
1543
1833
  async createRenderer(p) {
1544
- return createNodePainter({
1545
- width: p.width ?? width,
1546
- height: p.height ?? height,
1547
- pixelRatio: p.pixelRatio ?? pixelRatio
1548
- });
1834
+ try {
1835
+ return await createNodePainter({
1836
+ width: p.width ?? width,
1837
+ height: p.height ?? height,
1838
+ pixelRatio: p.pixelRatio ?? pixelRatio
1839
+ });
1840
+ } catch (err) {
1841
+ throw new Error(
1842
+ `Failed to create renderer: ${err instanceof Error ? err.message : String(err)}`
1843
+ );
1844
+ }
1549
1845
  },
1550
1846
  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);
1847
+ try {
1848
+ const finalOptions = {
1849
+ width: asset.width ?? width,
1850
+ height: asset.height ?? height,
1851
+ fps,
1852
+ duration: asset.animation?.duration ?? 3,
1853
+ outputPath: options.outputPath ?? "output.mp4",
1854
+ pixelRatio: asset.pixelRatio ?? pixelRatio
1855
+ };
1856
+ const frameGenerator = async (time) => {
1857
+ return this.renderFrame(asset, time);
1858
+ };
1859
+ await videoGenerator.generateVideo(frameGenerator, finalOptions);
1860
+ } catch (err) {
1861
+ throw new Error(
1862
+ `Failed to generate video: ${err instanceof Error ? err.message : String(err)}`
1863
+ );
1864
+ }
1563
1865
  },
1564
1866
  destroy() {
1565
- fonts.destroy();
1867
+ try {
1868
+ fonts.destroy();
1869
+ } catch (err) {
1870
+ console.error(`Error during cleanup: ${err instanceof Error ? err.message : String(err)}`);
1871
+ }
1566
1872
  }
1567
1873
  };
1568
1874
  }
1569
1875
  // Annotate the CommonJS export names for ESM import in node:
1570
1876
  0 && (module.exports = {
1571
- createTextEngine
1877
+ createTextEngine,
1878
+ isGlyphFill,
1879
+ isShadowFill
1572
1880
  });
1573
1881
  //# sourceMappingURL=entry.node.cjs.map