@shotstack/shotstack-canvas 1.0.9 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/entry.web.js CHANGED
@@ -132,31 +132,28 @@ var RichTextAssetSchema = Joi.object({
132
132
  var hbSingleton = null;
133
133
  async function initHB(wasmBaseURL) {
134
134
  if (hbSingleton) return hbSingleton;
135
- const harfbuzzjs = await import("harfbuzzjs");
136
- const factory = harfbuzzjs.default;
137
- let hbUrlFromImport;
138
135
  try {
139
- hbUrlFromImport = (await import("harfbuzzjs/hb.wasm?url")).default;
140
- } catch {
141
- }
142
- const base = (() => {
143
- if (wasmBaseURL) return wasmBaseURL.replace(/\/$/, "");
144
- if (hbUrlFromImport) return hbUrlFromImport.replace(/hb\.wasm(?:\?.*)?$/, "");
145
- return new URL(".", import.meta.url).toString().replace(/\/$/, "");
146
- })();
147
- const locateFile = (path, scriptDir) => {
148
- if (path.endsWith(".wasm")) {
149
- return `${base}/${path}`.replace(/([^:])\/{2,}/g, "$1/");
136
+ const mod = await import("harfbuzzjs");
137
+ const candidate = mod.default;
138
+ let hb;
139
+ if (typeof candidate === "function") {
140
+ hb = await candidate();
141
+ } else if (candidate && typeof candidate.then === "function") {
142
+ hb = await candidate;
143
+ } else {
144
+ hb = candidate;
150
145
  }
151
- return scriptDir ? scriptDir + path : path;
152
- };
153
- const initArg = { locateFile };
154
- const maybePromise = typeof factory === "function" ? factory(initArg) : factory;
155
- hbSingleton = maybePromise && typeof maybePromise.then === "function" ? await maybePromise : maybePromise;
156
- if (!hbSingleton || typeof hbSingleton.createBuffer !== "function") {
157
- throw new Error("Failed to initialize HarfBuzz: invalid API");
146
+ if (!hb || typeof hb.createBuffer !== "function" || typeof hb.createFont !== "function") {
147
+ throw new Error("Failed to initialize HarfBuzz: unexpected export shape from 'harfbuzzjs'.");
148
+ }
149
+ void wasmBaseURL;
150
+ hbSingleton = hb;
151
+ return hbSingleton;
152
+ } catch (err) {
153
+ throw new Error(
154
+ `Failed to initialize HarfBuzz: ${err instanceof Error ? err.message : String(err)}`
155
+ );
158
156
  }
159
- return hbSingleton;
160
157
  }
161
158
 
162
159
  // src/core/font-registry.ts
@@ -179,7 +176,13 @@ var FontRegistry = class {
179
176
  return;
180
177
  }
181
178
  this.initPromise = this._doInit();
182
- await this.initPromise;
179
+ try {
180
+ await this.initPromise;
181
+ } catch (err) {
182
+ throw new Error(
183
+ `Failed to initialize FontRegistry: ${err instanceof Error ? err.message : String(err)}`
184
+ );
185
+ }
183
186
  }
184
187
  async _doInit() {
185
188
  try {
@@ -191,7 +194,13 @@ var FontRegistry = class {
191
194
  }
192
195
  async getHB() {
193
196
  if (!this.hb) {
194
- await this.init();
197
+ try {
198
+ await this.init();
199
+ } catch (err) {
200
+ throw new Error(
201
+ `Failed to get HarfBuzz instance: ${err instanceof Error ? err.message : String(err)}`
202
+ );
203
+ }
195
204
  }
196
205
  return this.hb;
197
206
  }
@@ -199,48 +208,111 @@ var FontRegistry = class {
199
208
  return `${desc.family}__${desc.weight ?? "400"}__${desc.style ?? "normal"}`;
200
209
  }
201
210
  async registerFromBytes(bytes, desc) {
202
- if (!this.hb) await this.init();
203
- const k = this.key(desc);
204
- if (this.fonts.has(k)) return;
205
- const blob = this.hb.createBlob(bytes);
206
- const face = this.hb.createFace(blob, 0);
207
- const font = this.hb.createFont(face);
208
- const upem = face.upem || 1e3;
209
- font.setScale(upem, upem);
210
- this.blobs.set(k, blob);
211
- this.faces.set(k, face);
212
- this.fonts.set(k, font);
211
+ try {
212
+ if (!this.hb) await this.init();
213
+ const k = this.key(desc);
214
+ if (this.fonts.has(k)) return;
215
+ const blob = this.hb.createBlob(bytes);
216
+ const face = this.hb.createFace(blob, 0);
217
+ const font = this.hb.createFont(face);
218
+ const upem = face.upem || 1e3;
219
+ font.setScale(upem, upem);
220
+ this.blobs.set(k, blob);
221
+ this.faces.set(k, face);
222
+ this.fonts.set(k, font);
223
+ } catch (err) {
224
+ throw new Error(
225
+ `Failed to register font "${desc.family}": ${err instanceof Error ? err.message : String(err)}`
226
+ );
227
+ }
213
228
  }
214
229
  async getFont(desc) {
215
- if (!this.hb) await this.init();
216
- const k = this.key(desc);
217
- const f = this.fonts.get(k);
218
- if (!f) throw new Error(`Font not registered for ${k}`);
219
- return f;
230
+ try {
231
+ if (!this.hb) await this.init();
232
+ const k = this.key(desc);
233
+ const f = this.fonts.get(k);
234
+ if (!f) throw new Error(`Font not registered for ${k}`);
235
+ return f;
236
+ } catch (err) {
237
+ if (err instanceof Error && err.message.includes("Font not registered")) {
238
+ throw err;
239
+ }
240
+ throw new Error(
241
+ `Failed to get font "${desc.family}": ${err instanceof Error ? err.message : String(err)}`
242
+ );
243
+ }
220
244
  }
221
245
  async getFace(desc) {
222
- if (!this.hb) await this.init();
223
- const k = this.key(desc);
224
- return this.faces.get(k);
246
+ try {
247
+ if (!this.hb) await this.init();
248
+ const k = this.key(desc);
249
+ return this.faces.get(k);
250
+ } catch (err) {
251
+ throw new Error(
252
+ `Failed to get face for "${desc.family}": ${err instanceof Error ? err.message : String(err)}`
253
+ );
254
+ }
225
255
  }
226
256
  async getUnitsPerEm(desc) {
227
- const face = await this.getFace(desc);
228
- return face?.upem || 1e3;
257
+ try {
258
+ const face = await this.getFace(desc);
259
+ return face?.upem || 1e3;
260
+ } catch (err) {
261
+ throw new Error(
262
+ `Failed to get units per em for "${desc.family}": ${err instanceof Error ? err.message : String(err)}`
263
+ );
264
+ }
229
265
  }
230
266
  async glyphPath(desc, glyphId) {
231
- const font = await this.getFont(desc);
232
- const path = font.glyphToPath(glyphId);
233
- return path && path !== "" ? path : "M 0 0";
267
+ try {
268
+ const font = await this.getFont(desc);
269
+ const path = font.glyphToPath(glyphId);
270
+ return path && path !== "" ? path : "M 0 0";
271
+ } catch (err) {
272
+ throw new Error(
273
+ `Failed to get glyph path for glyph ${glyphId} in font "${desc.family}": ${err instanceof Error ? err.message : String(err)}`
274
+ );
275
+ }
234
276
  }
235
277
  destroy() {
236
- for (const [, f] of this.fonts) f.destroy?.();
237
- for (const [, f] of this.faces) f.destroy?.();
238
- for (const [, b] of this.blobs) b.destroy?.();
239
- this.fonts.clear();
240
- this.faces.clear();
241
- this.blobs.clear();
242
- this.hb = void 0;
243
- this.initPromise = void 0;
278
+ try {
279
+ for (const [, f] of this.fonts) {
280
+ try {
281
+ f.destroy();
282
+ } catch (err) {
283
+ console.error(
284
+ `Error destroying font: ${err instanceof Error ? err.message : String(err)}`
285
+ );
286
+ }
287
+ }
288
+ for (const [, f] of this.faces) {
289
+ try {
290
+ f.destroy();
291
+ } catch (err) {
292
+ console.error(
293
+ `Error destroying face: ${err instanceof Error ? err.message : String(err)}`
294
+ );
295
+ }
296
+ }
297
+ for (const [, b] of this.blobs) {
298
+ try {
299
+ b.destroy();
300
+ } catch (err) {
301
+ console.error(
302
+ `Error destroying blob: ${err instanceof Error ? err.message : String(err)}`
303
+ );
304
+ }
305
+ }
306
+ this.fonts.clear();
307
+ this.faces.clear();
308
+ this.blobs.clear();
309
+ this.hb = void 0;
310
+ this.initPromise = void 0;
311
+ } catch (err) {
312
+ console.error(
313
+ `Error during FontRegistry cleanup: ${err instanceof Error ? err.message : String(err)}`
314
+ );
315
+ }
244
316
  }
245
317
  };
246
318
 
@@ -262,112 +334,145 @@ var LayoutEngine = class {
262
334
  }
263
335
  }
264
336
  async shapeFull(text, desc) {
265
- const hb = await this.fonts.getHB();
266
- const buffer = hb.createBuffer();
267
- buffer.addText(text);
268
- buffer.guessSegmentProperties();
269
- const font = await this.fonts.getFont(desc);
270
- const face = await this.fonts.getFace(desc);
271
- const upem = face?.upem || 1e3;
272
- font.setScale(upem, upem);
273
- hb.shape(font, buffer);
274
- const result = buffer.json();
275
- buffer.destroy();
276
- return result;
337
+ try {
338
+ const hb = await this.fonts.getHB();
339
+ const buffer = hb.createBuffer();
340
+ try {
341
+ buffer.addText(text);
342
+ buffer.guessSegmentProperties();
343
+ const font = await this.fonts.getFont(desc);
344
+ const face = await this.fonts.getFace(desc);
345
+ const upem = face?.upem || 1e3;
346
+ font.setScale(upem, upem);
347
+ hb.shape(font, buffer);
348
+ const result = buffer.json();
349
+ return result;
350
+ } finally {
351
+ try {
352
+ buffer.destroy();
353
+ } catch (err) {
354
+ console.error(
355
+ `Error destroying HarfBuzz buffer: ${err instanceof Error ? err.message : String(err)}`
356
+ );
357
+ }
358
+ }
359
+ } catch (err) {
360
+ throw new Error(
361
+ `Failed to shape text with font "${desc.family}": ${err instanceof Error ? err.message : String(err)}`
362
+ );
363
+ }
277
364
  }
278
365
  async layout(params) {
279
- const { textTransform, desc, fontSize, letterSpacing, width } = params;
280
- const input = this.transformText(params.text, textTransform);
281
- if (!input || input.length === 0) {
282
- return [];
283
- }
284
- const shaped = await this.shapeFull(input, desc);
285
- const face = await this.fonts.getFace(desc);
286
- const upem = face?.upem || 1e3;
287
- const scale = fontSize / upem;
288
- const glyphs = shaped.map((g) => {
289
- const charIndex = g.cl;
290
- let char;
291
- if (charIndex >= 0 && charIndex < input.length) {
292
- char = input[charIndex];
366
+ try {
367
+ const { textTransform, desc, fontSize, letterSpacing, width } = params;
368
+ const input = this.transformText(params.text, textTransform);
369
+ if (!input || input.length === 0) {
370
+ return [];
293
371
  }
294
- return {
295
- id: g.g,
296
- xAdvance: g.ax * scale + letterSpacing,
297
- xOffset: g.dx * scale,
298
- yOffset: -g.dy * scale,
299
- cluster: g.cl,
300
- char
301
- // This now correctly maps to the original character
302
- };
303
- });
304
- const lines = [];
305
- let currentLine = [];
306
- let currentWidth = 0;
307
- const spaceIndices = /* @__PURE__ */ new Set();
308
- for (let i = 0; i < input.length; i++) {
309
- if (input[i] === " ") {
310
- spaceIndices.add(i);
372
+ let shaped;
373
+ try {
374
+ shaped = await this.shapeFull(input, desc);
375
+ } catch (err) {
376
+ throw new Error(`Text shaping failed: ${err instanceof Error ? err.message : String(err)}`);
311
377
  }
312
- }
313
- let lastBreakIndex = -1;
314
- for (let i = 0; i < glyphs.length; i++) {
315
- const glyph = glyphs[i];
316
- const glyphWidth = glyph.xAdvance;
317
- if (glyph.char === "\n") {
318
- if (currentLine.length > 0) {
319
- lines.push({
320
- glyphs: currentLine,
321
- width: currentWidth,
322
- y: 0
323
- });
378
+ let upem;
379
+ try {
380
+ const face = await this.fonts.getFace(desc);
381
+ upem = face?.upem || 1e3;
382
+ } catch (err) {
383
+ throw new Error(
384
+ `Failed to get font metrics: ${err instanceof Error ? err.message : String(err)}`
385
+ );
386
+ }
387
+ const scale = fontSize / upem;
388
+ const glyphs = shaped.map((g) => {
389
+ const charIndex = g.cl;
390
+ let char;
391
+ if (charIndex >= 0 && charIndex < input.length) {
392
+ char = input[charIndex];
393
+ }
394
+ return {
395
+ id: g.g,
396
+ xAdvance: g.ax * scale + letterSpacing,
397
+ xOffset: g.dx * scale,
398
+ yOffset: -g.dy * scale,
399
+ cluster: g.cl,
400
+ char
401
+ };
402
+ });
403
+ const lines = [];
404
+ let currentLine = [];
405
+ let currentWidth = 0;
406
+ const spaceIndices = /* @__PURE__ */ new Set();
407
+ for (let i = 0; i < input.length; i++) {
408
+ if (input[i] === " ") {
409
+ spaceIndices.add(i);
324
410
  }
325
- currentLine = [];
326
- currentWidth = 0;
327
- lastBreakIndex = i;
328
- continue;
329
411
  }
330
- if (currentWidth + glyphWidth > width && currentLine.length > 0) {
331
- if (lastBreakIndex > -1) {
332
- const breakPoint = lastBreakIndex - (i - currentLine.length) + 1;
333
- const nextLine = currentLine.splice(breakPoint);
334
- const lineWidth = currentLine.reduce((sum, g) => sum + g.xAdvance, 0);
335
- lines.push({
336
- glyphs: currentLine,
337
- width: lineWidth,
338
- y: 0
339
- });
340
- currentLine = nextLine;
341
- currentWidth = nextLine.reduce((sum, g) => sum + g.xAdvance, 0);
342
- } else {
343
- lines.push({
344
- glyphs: currentLine,
345
- width: currentWidth,
346
- y: 0
347
- });
412
+ let lastBreakIndex = -1;
413
+ for (let i = 0; i < glyphs.length; i++) {
414
+ const glyph = glyphs[i];
415
+ const glyphWidth = glyph.xAdvance;
416
+ if (glyph.char === "\n") {
417
+ if (currentLine.length > 0) {
418
+ lines.push({
419
+ glyphs: currentLine,
420
+ width: currentWidth,
421
+ y: 0
422
+ });
423
+ }
348
424
  currentLine = [];
349
425
  currentWidth = 0;
426
+ lastBreakIndex = i;
427
+ continue;
428
+ }
429
+ if (currentWidth + glyphWidth > width && currentLine.length > 0) {
430
+ if (lastBreakIndex > -1) {
431
+ const breakPoint = lastBreakIndex - (i - currentLine.length) + 1;
432
+ const nextLine = currentLine.splice(breakPoint);
433
+ const lineWidth = currentLine.reduce((sum, g) => sum + g.xAdvance, 0);
434
+ lines.push({
435
+ glyphs: currentLine,
436
+ width: lineWidth,
437
+ y: 0
438
+ });
439
+ currentLine = nextLine;
440
+ currentWidth = nextLine.reduce((sum, g) => sum + g.xAdvance, 0);
441
+ } else {
442
+ lines.push({
443
+ glyphs: currentLine,
444
+ width: currentWidth,
445
+ y: 0
446
+ });
447
+ currentLine = [];
448
+ currentWidth = 0;
449
+ }
450
+ lastBreakIndex = -1;
451
+ }
452
+ currentLine.push(glyph);
453
+ currentWidth += glyphWidth;
454
+ if (spaceIndices.has(glyph.cluster)) {
455
+ lastBreakIndex = i;
350
456
  }
351
- lastBreakIndex = -1;
352
457
  }
353
- currentLine.push(glyph);
354
- currentWidth += glyphWidth;
355
- if (spaceIndices.has(glyph.cluster)) {
356
- lastBreakIndex = i;
458
+ if (currentLine.length > 0) {
459
+ lines.push({
460
+ glyphs: currentLine,
461
+ width: currentWidth,
462
+ y: 0
463
+ });
357
464
  }
465
+ const lineHeight = params.lineHeight * fontSize;
466
+ for (let i = 0; i < lines.length; i++) {
467
+ lines[i].y = (i + 1) * lineHeight;
468
+ }
469
+ return lines;
470
+ } catch (err) {
471
+ if (err instanceof Error) {
472
+ throw err;
473
+ }
474
+ throw new Error(`Layout failed: ${String(err)}`);
358
475
  }
359
- if (currentLine.length > 0) {
360
- lines.push({
361
- glyphs: currentLine,
362
- width: currentWidth,
363
- y: 0
364
- });
365
- }
366
- const lineHeight = params.lineHeight * fontSize;
367
- for (let i = 0; i < lines.length; i++) {
368
- lines[i].y = (i + 1) * lineHeight;
369
- }
370
- return lines;
371
476
  }
372
477
  };
373
478
 
@@ -459,7 +564,6 @@ async function buildDrawOps(p) {
459
564
  path,
460
565
  x: glyphX + p.shadow.offsetX,
461
566
  y: glyphY + p.shadow.offsetY,
462
- // @ts-ignore scale propagated to painters
463
567
  scale,
464
568
  fill: { kind: "solid", color: p.shadow.color, opacity: p.shadow.opacity }
465
569
  });
@@ -470,7 +574,6 @@ async function buildDrawOps(p) {
470
574
  path,
471
575
  x: glyphX,
472
576
  y: glyphY,
473
- // @ts-ignore scale propagated to painters
474
577
  scale,
475
578
  width: p.stroke.width,
476
579
  color: p.stroke.color,
@@ -482,7 +585,6 @@ async function buildDrawOps(p) {
482
585
  path,
483
586
  x: glyphX,
484
587
  y: glyphY,
485
- // @ts-ignore scale propagated to painters
486
588
  scale,
487
589
  fill
488
590
  });
@@ -1025,7 +1127,8 @@ function createWebPainter(canvas) {
1025
1127
  for (const op of ops) {
1026
1128
  if (op.op === "BeginFrame") {
1027
1129
  const dpr = op.pixelRatio;
1028
- const w = op.width, h = op.height;
1130
+ const w = op.width;
1131
+ const h = op.height;
1029
1132
  if ("width" in canvas && "height" in canvas) {
1030
1133
  canvas.width = Math.floor(w * dpr);
1031
1134
  canvas.height = Math.floor(h * dpr);
@@ -1048,28 +1151,30 @@ function createWebPainter(canvas) {
1048
1151
  continue;
1049
1152
  }
1050
1153
  if (op.op === "FillPath") {
1051
- const p = new Path2D(op.path);
1154
+ const fillOp = op;
1155
+ const p = new Path2D(fillOp.path);
1052
1156
  ctx.save();
1053
- ctx.translate(op.x, op.y);
1054
- const s = op.scale ?? 1;
1157
+ ctx.translate(fillOp.x, fillOp.y);
1158
+ const s = fillOp.scale ?? 1;
1055
1159
  ctx.scale(s, -s);
1056
- const bbox = op.gradientBBox ?? globalBox;
1057
- const fill = makeGradientFromBBox(ctx, op.fill, bbox);
1160
+ const bbox = fillOp.gradientBBox ?? globalBox;
1161
+ const fill = makeGradientFromBBox(ctx, fillOp.fill, bbox);
1058
1162
  ctx.fillStyle = fill;
1059
1163
  ctx.fill(p);
1060
1164
  ctx.restore();
1061
1165
  continue;
1062
1166
  }
1063
1167
  if (op.op === "StrokePath") {
1064
- const p = new Path2D(op.path);
1168
+ const strokeOp = op;
1169
+ const p = new Path2D(strokeOp.path);
1065
1170
  ctx.save();
1066
- ctx.translate(op.x, op.y);
1067
- const s = op.scale ?? 1;
1171
+ ctx.translate(strokeOp.x, strokeOp.y);
1172
+ const s = strokeOp.scale ?? 1;
1068
1173
  ctx.scale(s, -s);
1069
1174
  const invAbs = 1 / Math.abs(s);
1070
- const c = parseHex6(op.color, op.opacity);
1175
+ const c = parseHex6(strokeOp.color, strokeOp.opacity);
1071
1176
  ctx.strokeStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
1072
- ctx.lineWidth = op.width * invAbs;
1177
+ ctx.lineWidth = strokeOp.width * invAbs;
1073
1178
  ctx.lineJoin = "round";
1074
1179
  ctx.lineCap = "round";
1075
1180
  ctx.stroke(p);
@@ -1109,17 +1214,20 @@ function makeGradientFromBBox(ctx, spec, box) {
1109
1214
  const c = parseHex6(spec.color, spec.opacity);
1110
1215
  return `rgba(${c.r},${c.g},${c.b},${c.a})`;
1111
1216
  }
1112
- const cx = box.x + box.w / 2, cy = box.y + box.h / 2, r = Math.max(box.w, box.h) / 2;
1217
+ const cx = box.x + box.w / 2;
1218
+ const cy = box.y + box.h / 2;
1219
+ const r = Math.max(box.w, box.h) / 2;
1113
1220
  const addStops = (g) => {
1114
- const op = spec.opacity ?? 1;
1115
- for (const s of spec.stops) {
1116
- const c = parseHex6(s.color, op);
1221
+ const opacity = spec.kind === "linear" || spec.kind === "radial" ? spec.opacity : 1;
1222
+ const stops = spec.kind === "linear" || spec.kind === "radial" ? spec.stops : [];
1223
+ for (const s of stops) {
1224
+ const c = parseHex6(s.color, opacity);
1117
1225
  g.addColorStop(s.offset, `rgba(${c.r},${c.g},${c.b},${c.a})`);
1118
1226
  }
1119
1227
  return g;
1120
1228
  };
1121
1229
  if (spec.kind === "linear") {
1122
- const rad = (spec.angle || 0) * Math.PI / 180;
1230
+ const rad = spec.angle * Math.PI / 180;
1123
1231
  const x1 = cx + Math.cos(rad + Math.PI) * r;
1124
1232
  const y1 = cy + Math.sin(rad + Math.PI) * r;
1125
1233
  const x2 = cx + Math.cos(rad) * r;
@@ -1130,27 +1238,42 @@ function makeGradientFromBBox(ctx, spec, box) {
1130
1238
  }
1131
1239
  }
1132
1240
  function computeGlobalTextBounds(ops) {
1133
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1241
+ let minX = Infinity;
1242
+ let minY = Infinity;
1243
+ let maxX = -Infinity;
1244
+ let maxY = -Infinity;
1134
1245
  for (const op of ops) {
1135
- if (op.op !== "FillPath" || op.isShadow) continue;
1136
- const b = computePathBounds2(op.path);
1137
- const s = op.scale ?? 1;
1138
- const x1 = op.x + s * b.x;
1139
- const x2 = op.x + s * (b.x + b.w);
1140
- const y1 = op.y - s * (b.y + b.h);
1141
- const y2 = op.y - s * b.y;
1246
+ if (op.op !== "FillPath") continue;
1247
+ const fillOp = op;
1248
+ if (fillOp.isShadow) continue;
1249
+ const b = computePathBounds2(fillOp.path);
1250
+ const s = fillOp.scale ?? 1;
1251
+ const x1 = fillOp.x + s * b.x;
1252
+ const x2 = fillOp.x + s * (b.x + b.w);
1253
+ const y1 = fillOp.y - s * (b.y + b.h);
1254
+ const y2 = fillOp.y - s * b.y;
1142
1255
  if (x1 < minX) minX = x1;
1143
1256
  if (y1 < minY) minY = y1;
1144
1257
  if (x2 > maxX) maxX = x2;
1145
1258
  if (y2 > maxY) maxY = y2;
1146
1259
  }
1147
- if (minX === Infinity) return { x: 0, y: 0, w: 1, h: 1 };
1148
- return { x: minX, y: minY, w: Math.max(1, maxX - minX), h: Math.max(1, maxY - minY) };
1260
+ if (minX === Infinity) {
1261
+ return { x: 0, y: 0, w: 1, h: 1 };
1262
+ }
1263
+ return {
1264
+ x: minX,
1265
+ y: minY,
1266
+ w: Math.max(1, maxX - minX),
1267
+ h: Math.max(1, maxY - minY)
1268
+ };
1149
1269
  }
1150
1270
  function computePathBounds2(d) {
1151
1271
  const tokens = tokenizePath2(d);
1152
1272
  let i = 0;
1153
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1273
+ let minX = Infinity;
1274
+ let minY = Infinity;
1275
+ let maxX = -Infinity;
1276
+ let maxY = -Infinity;
1154
1277
  const touch = (x, y) => {
1155
1278
  if (x < minX) minX = x;
1156
1279
  if (y < minY) minY = y;
@@ -1190,10 +1313,19 @@ function computePathBounds2(d) {
1190
1313
  }
1191
1314
  case "Z":
1192
1315
  break;
1316
+ default:
1317
+ break;
1193
1318
  }
1194
1319
  }
1195
- if (minX === Infinity) return { x: 0, y: 0, w: 0, h: 0 };
1196
- return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
1320
+ if (minX === Infinity) {
1321
+ return { x: 0, y: 0, w: 0, h: 0 };
1322
+ }
1323
+ return {
1324
+ x: minX,
1325
+ y: minY,
1326
+ w: maxX - minX,
1327
+ h: maxY - minY
1328
+ };
1197
1329
  }
1198
1330
  function tokenizePath2(d) {
1199
1331
  return d.match(/[MLCQZ]|-?\d*\.?\d+(?:e[-+]?\d+)?/gi) ?? [];
@@ -1201,11 +1333,37 @@ function tokenizePath2(d) {
1201
1333
 
1202
1334
  // src/io/web.ts
1203
1335
  async function fetchToArrayBuffer(url) {
1204
- const res = await fetch(url);
1205
- if (!res.ok) throw new Error(`Failed to fetch ${url}: ${res.status}`);
1206
- return await res.arrayBuffer();
1336
+ try {
1337
+ const res = await fetch(url);
1338
+ if (!res.ok) {
1339
+ throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`);
1340
+ }
1341
+ try {
1342
+ return await res.arrayBuffer();
1343
+ } catch (err) {
1344
+ throw new Error(
1345
+ `Failed to read response body as ArrayBuffer from ${url}: ${err instanceof Error ? err.message : String(err)}`
1346
+ );
1347
+ }
1348
+ } catch (err) {
1349
+ if (err instanceof Error) {
1350
+ if (err.message.includes("Failed to fetch") || err.message.includes("Failed to read")) {
1351
+ throw err;
1352
+ }
1353
+ throw new Error(`Failed to fetch ${url}: ${err.message}`);
1354
+ }
1355
+ throw new Error(`Failed to fetch ${url}: ${String(err)}`);
1356
+ }
1207
1357
  }
1208
1358
 
1359
+ // src/types.ts
1360
+ var isShadowFill2 = (op) => {
1361
+ return op.op === "FillPath" && op.isShadow === true;
1362
+ };
1363
+ var isGlyphFill2 = (op) => {
1364
+ return op.op === "FillPath" && op.isShadow !== true;
1365
+ };
1366
+
1209
1367
  // src/env/entry.web.ts
1210
1368
  async function createTextEngine(opts = {}) {
1211
1369
  const width = opts.width ?? CANVAS_CONFIG.DEFAULTS.width;
@@ -1214,109 +1372,206 @@ async function createTextEngine(opts = {}) {
1214
1372
  const wasmBaseURL = opts.wasmBaseURL;
1215
1373
  const fonts = new FontRegistry(wasmBaseURL);
1216
1374
  const layout = new LayoutEngine(fonts);
1217
- await fonts.init();
1375
+ try {
1376
+ await fonts.init();
1377
+ } catch (err) {
1378
+ throw new Error(
1379
+ `Failed to initialize font registry: ${err instanceof Error ? err.message : String(err)}`
1380
+ );
1381
+ }
1218
1382
  async function ensureFonts(asset) {
1219
- if (asset.customFonts) {
1220
- for (const cf of asset.customFonts) {
1221
- const bytes = await fetchToArrayBuffer(cf.src);
1222
- await fonts.registerFromBytes(bytes, {
1223
- family: cf.family,
1224
- weight: cf.weight ?? "400",
1225
- style: cf.style ?? "normal"
1226
- });
1383
+ try {
1384
+ if (asset.customFonts) {
1385
+ for (const cf of asset.customFonts) {
1386
+ try {
1387
+ const bytes = await fetchToArrayBuffer(cf.src);
1388
+ await fonts.registerFromBytes(bytes, {
1389
+ family: cf.family,
1390
+ weight: cf.weight ?? "400",
1391
+ style: cf.style ?? "normal"
1392
+ });
1393
+ } catch (err) {
1394
+ throw new Error(
1395
+ `Failed to load custom font "${cf.family}" from ${cf.src}: ${err instanceof Error ? err.message : String(err)}`
1396
+ );
1397
+ }
1398
+ }
1399
+ }
1400
+ const main = asset.font ?? {
1401
+ family: "Roboto",
1402
+ weight: "400",
1403
+ style: "normal",
1404
+ size: 48,
1405
+ color: "#000000",
1406
+ opacity: 1
1407
+ };
1408
+ return main;
1409
+ } catch (err) {
1410
+ if (err instanceof Error) {
1411
+ throw err;
1227
1412
  }
1413
+ throw new Error(`Failed to ensure fonts: ${String(err)}`);
1228
1414
  }
1229
- const main = asset.font ?? {
1230
- family: "Roboto",
1231
- weight: "400",
1232
- style: "normal",
1233
- size: 48,
1234
- color: "#000000",
1235
- opacity: 1
1236
- };
1237
- return main;
1238
1415
  }
1239
1416
  return {
1240
1417
  validate(input) {
1241
- const { value, error } = RichTextAssetSchema.validate(input, {
1242
- abortEarly: false,
1243
- convert: true
1244
- });
1245
- if (error) throw error;
1246
- return { value };
1418
+ try {
1419
+ const { value, error } = RichTextAssetSchema.validate(input, {
1420
+ abortEarly: false,
1421
+ convert: true
1422
+ });
1423
+ if (error) throw error;
1424
+ return { value };
1425
+ } catch (err) {
1426
+ if (err instanceof Error) {
1427
+ throw new Error(`Validation failed: ${err.message}`);
1428
+ }
1429
+ throw new Error(`Validation failed: ${String(err)}`);
1430
+ }
1247
1431
  },
1248
1432
  async registerFontFromUrl(url, desc) {
1249
- const bytes = await fetchToArrayBuffer(url);
1250
- await fonts.registerFromBytes(bytes, desc);
1433
+ try {
1434
+ const bytes = await fetchToArrayBuffer(url);
1435
+ await fonts.registerFromBytes(bytes, desc);
1436
+ } catch (err) {
1437
+ throw new Error(
1438
+ `Failed to register font "${desc.family}" from URL ${url}: ${err instanceof Error ? err.message : String(err)}`
1439
+ );
1440
+ }
1251
1441
  },
1252
1442
  async registerFontFromFile(source, desc) {
1253
- let bytes;
1254
- if (typeof source === "string") {
1255
- bytes = await fetchToArrayBuffer(source);
1256
- } else {
1257
- bytes = await source.arrayBuffer();
1443
+ try {
1444
+ let bytes;
1445
+ if (typeof source === "string") {
1446
+ try {
1447
+ bytes = await fetchToArrayBuffer(source);
1448
+ } catch (err) {
1449
+ throw new Error(
1450
+ `Failed to fetch font from ${source}: ${err instanceof Error ? err.message : String(err)}`
1451
+ );
1452
+ }
1453
+ } else {
1454
+ try {
1455
+ bytes = await source.arrayBuffer();
1456
+ } catch (err) {
1457
+ throw new Error(
1458
+ `Failed to read Blob as ArrayBuffer: ${err instanceof Error ? err.message : String(err)}`
1459
+ );
1460
+ }
1461
+ }
1462
+ await fonts.registerFromBytes(bytes, desc);
1463
+ } catch (err) {
1464
+ if (err instanceof Error) {
1465
+ throw err;
1466
+ }
1467
+ throw new Error(
1468
+ `Failed to register font "${desc.family}" from ${typeof source === "string" ? source : "Blob"}: ${String(err)}`
1469
+ );
1258
1470
  }
1259
- await fonts.registerFromBytes(bytes, desc);
1260
1471
  },
1261
1472
  async renderFrame(asset, tSeconds) {
1262
- const main = await ensureFonts(asset);
1263
- const desc = { family: main.family, weight: `${main.weight}`, style: main.style };
1264
- const lines = await layout.layout({
1265
- text: asset.text,
1266
- width: asset.width ?? width,
1267
- letterSpacing: asset.style?.letterSpacing ?? 0,
1268
- fontSize: main.size,
1269
- lineHeight: asset.style?.lineHeight ?? 1.2,
1270
- desc,
1271
- textTransform: asset.style?.textTransform ?? "none"
1272
- });
1273
- const textRect = { x: 0, y: 0, width: asset.width ?? width, height: asset.height ?? height };
1274
- const ops0 = await buildDrawOps({
1275
- canvas: { width, height, pixelRatio },
1276
- textRect,
1277
- lines,
1278
- font: {
1279
- family: main.family,
1280
- size: main.size,
1281
- weight: `${main.weight}`,
1282
- style: main.style,
1283
- color: main.color,
1284
- opacity: main.opacity
1285
- },
1286
- style: {
1287
- lineHeight: asset.style?.lineHeight ?? 1.2,
1288
- textDecoration: asset.style?.textDecoration ?? "none",
1289
- gradient: asset.style?.gradient
1290
- },
1291
- stroke: asset.stroke,
1292
- shadow: asset.shadow,
1293
- align: asset.align ?? { horizontal: "left", vertical: "middle" },
1294
- background: asset.background,
1295
- glyphPathProvider: (gid) => fonts.glyphPath(desc, gid),
1296
- getUnitsPerEm: () => fonts.getUnitsPerEm(desc)
1297
- });
1298
- const ops = applyAnimation(ops0, lines, {
1299
- t: tSeconds,
1300
- fontSize: main.size,
1301
- anim: asset.animation ? {
1302
- preset: asset.animation.preset,
1303
- speed: asset.animation.speed,
1304
- duration: asset.animation.duration,
1305
- style: asset.animation.style,
1306
- direction: asset.animation.direction
1307
- } : void 0
1308
- });
1309
- return ops;
1473
+ try {
1474
+ const main = await ensureFonts(asset);
1475
+ const desc = { family: main.family, weight: `${main.weight}`, style: main.style };
1476
+ let lines;
1477
+ try {
1478
+ lines = await layout.layout({
1479
+ text: asset.text,
1480
+ width: asset.width ?? width,
1481
+ letterSpacing: asset.style?.letterSpacing ?? 0,
1482
+ fontSize: main.size,
1483
+ lineHeight: asset.style?.lineHeight ?? 1.2,
1484
+ desc,
1485
+ textTransform: asset.style?.textTransform ?? "none"
1486
+ });
1487
+ } catch (err) {
1488
+ throw new Error(
1489
+ `Failed to layout text: ${err instanceof Error ? err.message : String(err)}`
1490
+ );
1491
+ }
1492
+ const textRect = {
1493
+ x: 0,
1494
+ y: 0,
1495
+ width: asset.width ?? width,
1496
+ height: asset.height ?? height
1497
+ };
1498
+ let ops0;
1499
+ try {
1500
+ ops0 = await buildDrawOps({
1501
+ canvas: { width, height, pixelRatio },
1502
+ textRect,
1503
+ lines,
1504
+ font: {
1505
+ family: main.family,
1506
+ size: main.size,
1507
+ weight: `${main.weight}`,
1508
+ style: main.style,
1509
+ color: main.color,
1510
+ opacity: main.opacity
1511
+ },
1512
+ style: {
1513
+ lineHeight: asset.style?.lineHeight ?? 1.2,
1514
+ textDecoration: asset.style?.textDecoration ?? "none",
1515
+ gradient: asset.style?.gradient
1516
+ },
1517
+ stroke: asset.stroke,
1518
+ shadow: asset.shadow,
1519
+ align: asset.align ?? { horizontal: "left", vertical: "middle" },
1520
+ background: asset.background,
1521
+ glyphPathProvider: (gid) => fonts.glyphPath(desc, gid),
1522
+ getUnitsPerEm: () => fonts.getUnitsPerEm(desc)
1523
+ });
1524
+ } catch (err) {
1525
+ throw new Error(
1526
+ `Failed to build draw operations: ${err instanceof Error ? err.message : String(err)}`
1527
+ );
1528
+ }
1529
+ try {
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;
1542
+ } catch (err) {
1543
+ throw new Error(
1544
+ `Failed to apply animation: ${err instanceof Error ? err.message : String(err)}`
1545
+ );
1546
+ }
1547
+ } catch (err) {
1548
+ if (err instanceof Error) {
1549
+ throw err;
1550
+ }
1551
+ throw new Error(`Failed to render frame at time ${tSeconds}s: ${String(err)}`);
1552
+ }
1310
1553
  },
1311
1554
  createRenderer(canvas) {
1312
- return createWebPainter(canvas);
1555
+ try {
1556
+ return createWebPainter(canvas);
1557
+ } catch (err) {
1558
+ throw new Error(
1559
+ `Failed to create renderer: ${err instanceof Error ? err.message : String(err)}`
1560
+ );
1561
+ }
1313
1562
  },
1314
1563
  destroy() {
1315
- fonts.destroy();
1564
+ try {
1565
+ fonts.destroy();
1566
+ } catch (err) {
1567
+ console.error(`Error during cleanup: ${err instanceof Error ? err.message : String(err)}`);
1568
+ }
1316
1569
  }
1317
1570
  };
1318
1571
  }
1319
1572
  export {
1320
- createTextEngine
1573
+ createTextEngine,
1574
+ isGlyphFill2 as isGlyphFill,
1575
+ isShadowFill2 as isShadowFill
1321
1576
  };
1322
1577
  //# sourceMappingURL=entry.web.js.map