@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.
package/dist/entry.web.js CHANGED
@@ -132,23 +132,32 @@ 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 hbPromise = harfbuzzjs.default;
137
- if (typeof hbPromise === "function") {
138
- hbSingleton = await hbPromise();
139
- } else if (hbPromise && typeof hbPromise.then === "function") {
140
- hbSingleton = await hbPromise;
141
- } else {
142
- hbSingleton = hbPromise;
143
- }
144
- if (!hbSingleton || typeof hbSingleton.createBuffer !== "function") {
145
- throw new Error("Failed to initialize HarfBuzz: invalid API");
135
+ try {
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;
145
+ }
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
+ );
146
156
  }
147
- return hbSingleton;
148
157
  }
149
158
 
150
159
  // src/core/font-registry.ts
151
- var FontRegistry = class {
160
+ var _FontRegistry = class _FontRegistry {
152
161
  constructor(wasmBaseURL) {
153
162
  __publicField(this, "hb");
154
163
  __publicField(this, "faces", /* @__PURE__ */ new Map());
@@ -158,16 +167,23 @@ var FontRegistry = class {
158
167
  __publicField(this, "initPromise");
159
168
  this.wasmBaseURL = wasmBaseURL;
160
169
  }
170
+ static setFallbackLoader(loader) {
171
+ _FontRegistry.fallbackLoader = loader;
172
+ }
161
173
  async init() {
162
174
  if (this.initPromise) {
163
175
  await this.initPromise;
164
176
  return;
165
177
  }
166
- if (this.hb) {
167
- return;
168
- }
178
+ if (this.hb) return;
169
179
  this.initPromise = this._doInit();
170
- await this.initPromise;
180
+ try {
181
+ await this.initPromise;
182
+ } catch (err) {
183
+ throw new Error(
184
+ `Failed to initialize FontRegistry: ${err instanceof Error ? err.message : String(err)}`
185
+ );
186
+ }
171
187
  }
172
188
  async _doInit() {
173
189
  try {
@@ -179,7 +195,13 @@ var FontRegistry = class {
179
195
  }
180
196
  async getHB() {
181
197
  if (!this.hb) {
182
- await this.init();
198
+ try {
199
+ await this.init();
200
+ } catch (err) {
201
+ throw new Error(
202
+ `Failed to get HarfBuzz instance: ${err instanceof Error ? err.message : String(err)}`
203
+ );
204
+ }
183
205
  }
184
206
  return this.hb;
185
207
  }
@@ -187,50 +209,144 @@ var FontRegistry = class {
187
209
  return `${desc.family}__${desc.weight ?? "400"}__${desc.style ?? "normal"}`;
188
210
  }
189
211
  async registerFromBytes(bytes, desc) {
190
- if (!this.hb) await this.init();
191
- const k = this.key(desc);
192
- if (this.fonts.has(k)) return;
193
- const blob = this.hb.createBlob(bytes);
194
- const face = this.hb.createFace(blob, 0);
195
- const font = this.hb.createFont(face);
196
- const upem = face.upem || 1e3;
197
- font.setScale(upem, upem);
198
- this.blobs.set(k, blob);
199
- this.faces.set(k, face);
200
- this.fonts.set(k, font);
212
+ try {
213
+ if (!this.hb) await this.init();
214
+ const k = this.key(desc);
215
+ if (this.fonts.has(k)) return;
216
+ const blob = this.hb.createBlob(bytes);
217
+ const face = this.hb.createFace(blob, 0);
218
+ const font = this.hb.createFont(face);
219
+ const upem = face.upem || 1e3;
220
+ font.setScale(upem, upem);
221
+ this.blobs.set(k, blob);
222
+ this.faces.set(k, face);
223
+ this.fonts.set(k, font);
224
+ } catch (err) {
225
+ throw new Error(
226
+ `Failed to register font "${desc.family}": ${err instanceof Error ? err.message : String(err)}`
227
+ );
228
+ }
229
+ }
230
+ async tryFallbackInstall(desc) {
231
+ const loader = _FontRegistry.fallbackLoader;
232
+ if (!loader) return false;
233
+ try {
234
+ const bytes = await loader({
235
+ family: desc.family,
236
+ weight: desc.weight ?? "400",
237
+ style: desc.style ?? "normal"
238
+ });
239
+ if (!bytes) return false;
240
+ await this.registerFromBytes(bytes, {
241
+ family: desc.family,
242
+ weight: desc.weight ?? "400",
243
+ style: desc.style ?? "normal"
244
+ });
245
+ return true;
246
+ } catch {
247
+ return false;
248
+ }
201
249
  }
202
250
  async getFont(desc) {
203
- if (!this.hb) await this.init();
204
- const k = this.key(desc);
205
- const f = this.fonts.get(k);
206
- if (!f) throw new Error(`Font not registered for ${k}`);
207
- return f;
251
+ try {
252
+ if (!this.hb) await this.init();
253
+ const k = this.key(desc);
254
+ let f = this.fonts.get(k);
255
+ if (!f) {
256
+ const installed = await this.tryFallbackInstall(desc);
257
+ f = installed ? this.fonts.get(k) : void 0;
258
+ }
259
+ if (!f) throw new Error(`Font not registered for ${k}`);
260
+ return f;
261
+ } catch (err) {
262
+ if (err instanceof Error && err.message.includes("Font not registered")) {
263
+ throw err;
264
+ }
265
+ throw new Error(
266
+ `Failed to get font "${desc.family}": ${err instanceof Error ? err.message : String(err)}`
267
+ );
268
+ }
208
269
  }
209
270
  async getFace(desc) {
210
- if (!this.hb) await this.init();
211
- const k = this.key(desc);
212
- return this.faces.get(k);
271
+ try {
272
+ if (!this.hb) await this.init();
273
+ const k = this.key(desc);
274
+ let face = this.faces.get(k);
275
+ if (!face) {
276
+ const installed = await this.tryFallbackInstall(desc);
277
+ face = installed ? this.faces.get(k) : void 0;
278
+ }
279
+ return face;
280
+ } catch (err) {
281
+ throw new Error(
282
+ `Failed to get face for "${desc.family}": ${err instanceof Error ? err.message : String(err)}`
283
+ );
284
+ }
213
285
  }
214
286
  async getUnitsPerEm(desc) {
215
- const face = await this.getFace(desc);
216
- return face?.upem || 1e3;
287
+ try {
288
+ const face = await this.getFace(desc);
289
+ return face?.upem || 1e3;
290
+ } catch (err) {
291
+ throw new Error(
292
+ `Failed to get units per em for "${desc.family}": ${err instanceof Error ? err.message : String(err)}`
293
+ );
294
+ }
217
295
  }
218
296
  async glyphPath(desc, glyphId) {
219
- const font = await this.getFont(desc);
220
- const path = font.glyphToPath(glyphId);
221
- return path && path !== "" ? path : "M 0 0";
297
+ try {
298
+ const font = await this.getFont(desc);
299
+ const path = font.glyphToPath(glyphId);
300
+ return path && path !== "" ? path : "M 0 0";
301
+ } catch (err) {
302
+ throw new Error(
303
+ `Failed to get glyph path for glyph ${glyphId} in font "${desc.family}": ${err instanceof Error ? err.message : String(err)}`
304
+ );
305
+ }
222
306
  }
223
307
  destroy() {
224
- for (const [, f] of this.fonts) f.destroy?.();
225
- for (const [, f] of this.faces) f.destroy?.();
226
- for (const [, b] of this.blobs) b.destroy?.();
227
- this.fonts.clear();
228
- this.faces.clear();
229
- this.blobs.clear();
230
- this.hb = void 0;
231
- this.initPromise = void 0;
308
+ try {
309
+ for (const [, f] of this.fonts) {
310
+ try {
311
+ f.destroy();
312
+ } catch (err) {
313
+ console.error(
314
+ `Error destroying font: ${err instanceof Error ? err.message : String(err)}`
315
+ );
316
+ }
317
+ }
318
+ for (const [, f] of this.faces) {
319
+ try {
320
+ f.destroy();
321
+ } catch (err) {
322
+ console.error(
323
+ `Error destroying face: ${err instanceof Error ? err.message : String(err)}`
324
+ );
325
+ }
326
+ }
327
+ for (const [, b] of this.blobs) {
328
+ try {
329
+ b.destroy();
330
+ } catch (err) {
331
+ console.error(
332
+ `Error destroying blob: ${err instanceof Error ? err.message : String(err)}`
333
+ );
334
+ }
335
+ }
336
+ this.fonts.clear();
337
+ this.faces.clear();
338
+ this.blobs.clear();
339
+ this.hb = void 0;
340
+ this.initPromise = void 0;
341
+ } catch (err) {
342
+ console.error(
343
+ `Error during FontRegistry cleanup: ${err instanceof Error ? err.message : String(err)}`
344
+ );
345
+ }
232
346
  }
233
347
  };
348
+ __publicField(_FontRegistry, "fallbackLoader");
349
+ var FontRegistry = _FontRegistry;
234
350
 
235
351
  // src/core/layout.ts
236
352
  var LayoutEngine = class {
@@ -250,112 +366,145 @@ var LayoutEngine = class {
250
366
  }
251
367
  }
252
368
  async shapeFull(text, desc) {
253
- const hb = await this.fonts.getHB();
254
- const buffer = hb.createBuffer();
255
- buffer.addText(text);
256
- buffer.guessSegmentProperties();
257
- const font = await this.fonts.getFont(desc);
258
- const face = await this.fonts.getFace(desc);
259
- const upem = face?.upem || 1e3;
260
- font.setScale(upem, upem);
261
- hb.shape(font, buffer);
262
- const result = buffer.json();
263
- buffer.destroy();
264
- return result;
369
+ try {
370
+ const hb = await this.fonts.getHB();
371
+ const buffer = hb.createBuffer();
372
+ try {
373
+ buffer.addText(text);
374
+ buffer.guessSegmentProperties();
375
+ const font = await this.fonts.getFont(desc);
376
+ const face = await this.fonts.getFace(desc);
377
+ const upem = face?.upem || 1e3;
378
+ font.setScale(upem, upem);
379
+ hb.shape(font, buffer);
380
+ const result = buffer.json();
381
+ return result;
382
+ } finally {
383
+ try {
384
+ buffer.destroy();
385
+ } catch (err) {
386
+ console.error(
387
+ `Error destroying HarfBuzz buffer: ${err instanceof Error ? err.message : String(err)}`
388
+ );
389
+ }
390
+ }
391
+ } catch (err) {
392
+ throw new Error(
393
+ `Failed to shape text with font "${desc.family}": ${err instanceof Error ? err.message : String(err)}`
394
+ );
395
+ }
265
396
  }
266
397
  async layout(params) {
267
- const { textTransform, desc, fontSize, letterSpacing, width } = params;
268
- const input = this.transformText(params.text, textTransform);
269
- if (!input || input.length === 0) {
270
- return [];
271
- }
272
- const shaped = await this.shapeFull(input, desc);
273
- const face = await this.fonts.getFace(desc);
274
- const upem = face?.upem || 1e3;
275
- const scale = fontSize / upem;
276
- const glyphs = shaped.map((g) => {
277
- const charIndex = g.cl;
278
- let char;
279
- if (charIndex >= 0 && charIndex < input.length) {
280
- char = input[charIndex];
398
+ try {
399
+ const { textTransform, desc, fontSize, letterSpacing, width } = params;
400
+ const input = this.transformText(params.text, textTransform);
401
+ if (!input || input.length === 0) {
402
+ return [];
281
403
  }
282
- return {
283
- id: g.g,
284
- xAdvance: g.ax * scale + letterSpacing,
285
- xOffset: g.dx * scale,
286
- yOffset: -g.dy * scale,
287
- cluster: g.cl,
288
- char
289
- // This now correctly maps to the original character
290
- };
291
- });
292
- const lines = [];
293
- let currentLine = [];
294
- let currentWidth = 0;
295
- const spaceIndices = /* @__PURE__ */ new Set();
296
- for (let i = 0; i < input.length; i++) {
297
- if (input[i] === " ") {
298
- spaceIndices.add(i);
404
+ let shaped;
405
+ try {
406
+ shaped = await this.shapeFull(input, desc);
407
+ } catch (err) {
408
+ throw new Error(`Text shaping failed: ${err instanceof Error ? err.message : String(err)}`);
299
409
  }
300
- }
301
- let lastBreakIndex = -1;
302
- for (let i = 0; i < glyphs.length; i++) {
303
- const glyph = glyphs[i];
304
- const glyphWidth = glyph.xAdvance;
305
- if (glyph.char === "\n") {
306
- if (currentLine.length > 0) {
307
- lines.push({
308
- glyphs: currentLine,
309
- width: currentWidth,
310
- y: 0
311
- });
410
+ let upem;
411
+ try {
412
+ const face = await this.fonts.getFace(desc);
413
+ upem = face?.upem || 1e3;
414
+ } catch (err) {
415
+ throw new Error(
416
+ `Failed to get font metrics: ${err instanceof Error ? err.message : String(err)}`
417
+ );
418
+ }
419
+ const scale = fontSize / upem;
420
+ const glyphs = shaped.map((g) => {
421
+ const charIndex = g.cl;
422
+ let char;
423
+ if (charIndex >= 0 && charIndex < input.length) {
424
+ char = input[charIndex];
425
+ }
426
+ return {
427
+ id: g.g,
428
+ xAdvance: g.ax * scale + letterSpacing,
429
+ xOffset: g.dx * scale,
430
+ yOffset: -g.dy * scale,
431
+ cluster: g.cl,
432
+ char
433
+ };
434
+ });
435
+ const lines = [];
436
+ let currentLine = [];
437
+ let currentWidth = 0;
438
+ const spaceIndices = /* @__PURE__ */ new Set();
439
+ for (let i = 0; i < input.length; i++) {
440
+ if (input[i] === " ") {
441
+ spaceIndices.add(i);
312
442
  }
313
- currentLine = [];
314
- currentWidth = 0;
315
- lastBreakIndex = i;
316
- continue;
317
443
  }
318
- if (currentWidth + glyphWidth > width && currentLine.length > 0) {
319
- if (lastBreakIndex > -1) {
320
- const breakPoint = lastBreakIndex - (i - currentLine.length) + 1;
321
- const nextLine = currentLine.splice(breakPoint);
322
- const lineWidth = currentLine.reduce((sum, g) => sum + g.xAdvance, 0);
323
- lines.push({
324
- glyphs: currentLine,
325
- width: lineWidth,
326
- y: 0
327
- });
328
- currentLine = nextLine;
329
- currentWidth = nextLine.reduce((sum, g) => sum + g.xAdvance, 0);
330
- } else {
331
- lines.push({
332
- glyphs: currentLine,
333
- width: currentWidth,
334
- y: 0
335
- });
444
+ let lastBreakIndex = -1;
445
+ for (let i = 0; i < glyphs.length; i++) {
446
+ const glyph = glyphs[i];
447
+ const glyphWidth = glyph.xAdvance;
448
+ if (glyph.char === "\n") {
449
+ if (currentLine.length > 0) {
450
+ lines.push({
451
+ glyphs: currentLine,
452
+ width: currentWidth,
453
+ y: 0
454
+ });
455
+ }
336
456
  currentLine = [];
337
457
  currentWidth = 0;
458
+ lastBreakIndex = i;
459
+ continue;
460
+ }
461
+ if (currentWidth + glyphWidth > width && currentLine.length > 0) {
462
+ if (lastBreakIndex > -1) {
463
+ const breakPoint = lastBreakIndex - (i - currentLine.length) + 1;
464
+ const nextLine = currentLine.splice(breakPoint);
465
+ const lineWidth = currentLine.reduce((sum, g) => sum + g.xAdvance, 0);
466
+ lines.push({
467
+ glyphs: currentLine,
468
+ width: lineWidth,
469
+ y: 0
470
+ });
471
+ currentLine = nextLine;
472
+ currentWidth = nextLine.reduce((sum, g) => sum + g.xAdvance, 0);
473
+ } else {
474
+ lines.push({
475
+ glyphs: currentLine,
476
+ width: currentWidth,
477
+ y: 0
478
+ });
479
+ currentLine = [];
480
+ currentWidth = 0;
481
+ }
482
+ lastBreakIndex = -1;
483
+ }
484
+ currentLine.push(glyph);
485
+ currentWidth += glyphWidth;
486
+ if (spaceIndices.has(glyph.cluster)) {
487
+ lastBreakIndex = i;
338
488
  }
339
- lastBreakIndex = -1;
340
489
  }
341
- currentLine.push(glyph);
342
- currentWidth += glyphWidth;
343
- if (spaceIndices.has(glyph.cluster)) {
344
- lastBreakIndex = i;
490
+ if (currentLine.length > 0) {
491
+ lines.push({
492
+ glyphs: currentLine,
493
+ width: currentWidth,
494
+ y: 0
495
+ });
345
496
  }
497
+ const lineHeight = params.lineHeight * fontSize;
498
+ for (let i = 0; i < lines.length; i++) {
499
+ lines[i].y = (i + 1) * lineHeight;
500
+ }
501
+ return lines;
502
+ } catch (err) {
503
+ if (err instanceof Error) {
504
+ throw err;
505
+ }
506
+ throw new Error(`Layout failed: ${String(err)}`);
346
507
  }
347
- if (currentLine.length > 0) {
348
- lines.push({
349
- glyphs: currentLine,
350
- width: currentWidth,
351
- y: 0
352
- });
353
- }
354
- const lineHeight = params.lineHeight * fontSize;
355
- for (let i = 0; i < lines.length; i++) {
356
- lines[i].y = (i + 1) * lineHeight;
357
- }
358
- return lines;
359
508
  }
360
509
  };
361
510
 
@@ -447,7 +596,6 @@ async function buildDrawOps(p) {
447
596
  path,
448
597
  x: glyphX + p.shadow.offsetX,
449
598
  y: glyphY + p.shadow.offsetY,
450
- // @ts-ignore scale propagated to painters
451
599
  scale,
452
600
  fill: { kind: "solid", color: p.shadow.color, opacity: p.shadow.opacity }
453
601
  });
@@ -458,7 +606,6 @@ async function buildDrawOps(p) {
458
606
  path,
459
607
  x: glyphX,
460
608
  y: glyphY,
461
- // @ts-ignore scale propagated to painters
462
609
  scale,
463
610
  width: p.stroke.width,
464
611
  color: p.stroke.color,
@@ -470,7 +617,6 @@ async function buildDrawOps(p) {
470
617
  path,
471
618
  x: glyphX,
472
619
  y: glyphY,
473
- // @ts-ignore scale propagated to painters
474
620
  scale,
475
621
  fill
476
622
  });
@@ -1013,11 +1159,16 @@ function createWebPainter(canvas) {
1013
1159
  for (const op of ops) {
1014
1160
  if (op.op === "BeginFrame") {
1015
1161
  const dpr = op.pixelRatio;
1016
- const w = op.width, h = op.height;
1162
+ const w = op.width;
1163
+ const h = op.height;
1017
1164
  if ("width" in canvas && "height" in canvas) {
1018
1165
  canvas.width = Math.floor(w * dpr);
1019
1166
  canvas.height = Math.floor(h * dpr);
1020
1167
  }
1168
+ if ("style" in canvas) {
1169
+ canvas.style.width = `${w}px`;
1170
+ canvas.style.height = `${h}px`;
1171
+ }
1021
1172
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
1022
1173
  if (op.clear) ctx.clearRect(0, 0, w, h);
1023
1174
  if (op.bg) {
@@ -1036,28 +1187,30 @@ function createWebPainter(canvas) {
1036
1187
  continue;
1037
1188
  }
1038
1189
  if (op.op === "FillPath") {
1039
- const p = new Path2D(op.path);
1190
+ const fillOp = op;
1191
+ const p = new Path2D(fillOp.path);
1040
1192
  ctx.save();
1041
- ctx.translate(op.x, op.y);
1042
- const s = op.scale ?? 1;
1193
+ ctx.translate(fillOp.x, fillOp.y);
1194
+ const s = fillOp.scale ?? 1;
1043
1195
  ctx.scale(s, -s);
1044
- const bbox = op.gradientBBox ?? globalBox;
1045
- const fill = makeGradientFromBBox(ctx, op.fill, bbox);
1196
+ const bbox = fillOp.gradientBBox ?? globalBox;
1197
+ const fill = makeGradientFromBBox(ctx, fillOp.fill, bbox);
1046
1198
  ctx.fillStyle = fill;
1047
1199
  ctx.fill(p);
1048
1200
  ctx.restore();
1049
1201
  continue;
1050
1202
  }
1051
1203
  if (op.op === "StrokePath") {
1052
- const p = new Path2D(op.path);
1204
+ const strokeOp = op;
1205
+ const p = new Path2D(strokeOp.path);
1053
1206
  ctx.save();
1054
- ctx.translate(op.x, op.y);
1055
- const s = op.scale ?? 1;
1207
+ ctx.translate(strokeOp.x, strokeOp.y);
1208
+ const s = strokeOp.scale ?? 1;
1056
1209
  ctx.scale(s, -s);
1057
1210
  const invAbs = 1 / Math.abs(s);
1058
- const c = parseHex6(op.color, op.opacity);
1211
+ const c = parseHex6(strokeOp.color, strokeOp.opacity);
1059
1212
  ctx.strokeStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
1060
- ctx.lineWidth = op.width * invAbs;
1213
+ ctx.lineWidth = strokeOp.width * invAbs;
1061
1214
  ctx.lineJoin = "round";
1062
1215
  ctx.lineCap = "round";
1063
1216
  ctx.stroke(p);
@@ -1097,17 +1250,20 @@ function makeGradientFromBBox(ctx, spec, box) {
1097
1250
  const c = parseHex6(spec.color, spec.opacity);
1098
1251
  return `rgba(${c.r},${c.g},${c.b},${c.a})`;
1099
1252
  }
1100
- const cx = box.x + box.w / 2, cy = box.y + box.h / 2, r = Math.max(box.w, box.h) / 2;
1253
+ const cx = box.x + box.w / 2;
1254
+ const cy = box.y + box.h / 2;
1255
+ const r = Math.max(box.w, box.h) / 2;
1101
1256
  const addStops = (g) => {
1102
- const op = spec.opacity ?? 1;
1103
- for (const s of spec.stops) {
1104
- const c = parseHex6(s.color, op);
1257
+ const opacity = spec.kind === "linear" || spec.kind === "radial" ? spec.opacity : 1;
1258
+ const stops = spec.kind === "linear" || spec.kind === "radial" ? spec.stops : [];
1259
+ for (const s of stops) {
1260
+ const c = parseHex6(s.color, opacity);
1105
1261
  g.addColorStop(s.offset, `rgba(${c.r},${c.g},${c.b},${c.a})`);
1106
1262
  }
1107
1263
  return g;
1108
1264
  };
1109
1265
  if (spec.kind === "linear") {
1110
- const rad = (spec.angle || 0) * Math.PI / 180;
1266
+ const rad = spec.angle * Math.PI / 180;
1111
1267
  const x1 = cx + Math.cos(rad + Math.PI) * r;
1112
1268
  const y1 = cy + Math.sin(rad + Math.PI) * r;
1113
1269
  const x2 = cx + Math.cos(rad) * r;
@@ -1118,27 +1274,42 @@ function makeGradientFromBBox(ctx, spec, box) {
1118
1274
  }
1119
1275
  }
1120
1276
  function computeGlobalTextBounds(ops) {
1121
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1277
+ let minX = Infinity;
1278
+ let minY = Infinity;
1279
+ let maxX = -Infinity;
1280
+ let maxY = -Infinity;
1122
1281
  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;
1282
+ if (op.op !== "FillPath") continue;
1283
+ const fillOp = op;
1284
+ if (fillOp.isShadow) continue;
1285
+ const b = computePathBounds2(fillOp.path);
1286
+ const s = fillOp.scale ?? 1;
1287
+ const x1 = fillOp.x + s * b.x;
1288
+ const x2 = fillOp.x + s * (b.x + b.w);
1289
+ const y1 = fillOp.y - s * (b.y + b.h);
1290
+ const y2 = fillOp.y - s * b.y;
1130
1291
  if (x1 < minX) minX = x1;
1131
1292
  if (y1 < minY) minY = y1;
1132
1293
  if (x2 > maxX) maxX = x2;
1133
1294
  if (y2 > maxY) maxY = y2;
1134
1295
  }
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) };
1296
+ if (minX === Infinity) {
1297
+ return { x: 0, y: 0, w: 1, h: 1 };
1298
+ }
1299
+ return {
1300
+ x: minX,
1301
+ y: minY,
1302
+ w: Math.max(1, maxX - minX),
1303
+ h: Math.max(1, maxY - minY)
1304
+ };
1137
1305
  }
1138
1306
  function computePathBounds2(d) {
1139
1307
  const tokens = tokenizePath2(d);
1140
1308
  let i = 0;
1141
- 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;
1142
1313
  const touch = (x, y) => {
1143
1314
  if (x < minX) minX = x;
1144
1315
  if (y < minY) minY = y;
@@ -1178,10 +1349,19 @@ function computePathBounds2(d) {
1178
1349
  }
1179
1350
  case "Z":
1180
1351
  break;
1352
+ default:
1353
+ break;
1181
1354
  }
1182
1355
  }
1183
- if (minX === Infinity) return { x: 0, y: 0, w: 0, h: 0 };
1184
- return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
1356
+ if (minX === Infinity) {
1357
+ return { x: 0, y: 0, w: 0, h: 0 };
1358
+ }
1359
+ return {
1360
+ x: minX,
1361
+ y: minY,
1362
+ w: maxX - minX,
1363
+ h: maxY - minY
1364
+ };
1185
1365
  }
1186
1366
  function tokenizePath2(d) {
1187
1367
  return d.match(/[MLCQZ]|-?\d*\.?\d+(?:e[-+]?\d+)?/gi) ?? [];
@@ -1189,12 +1369,39 @@ function tokenizePath2(d) {
1189
1369
 
1190
1370
  // src/io/web.ts
1191
1371
  async function fetchToArrayBuffer(url) {
1192
- const res = await fetch(url);
1193
- if (!res.ok) throw new Error(`Failed to fetch ${url}: ${res.status}`);
1194
- return await res.arrayBuffer();
1372
+ try {
1373
+ const res = await fetch(url);
1374
+ if (!res.ok) {
1375
+ throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`);
1376
+ }
1377
+ try {
1378
+ return await res.arrayBuffer();
1379
+ } catch (err) {
1380
+ throw new Error(
1381
+ `Failed to read response body as ArrayBuffer from ${url}: ${err instanceof Error ? err.message : String(err)}`
1382
+ );
1383
+ }
1384
+ } catch (err) {
1385
+ if (err instanceof Error) {
1386
+ if (err.message.includes("Failed to fetch") || err.message.includes("Failed to read")) {
1387
+ throw err;
1388
+ }
1389
+ throw new Error(`Failed to fetch ${url}: ${err.message}`);
1390
+ }
1391
+ throw new Error(`Failed to fetch ${url}: ${String(err)}`);
1392
+ }
1195
1393
  }
1196
1394
 
1395
+ // src/types.ts
1396
+ var isShadowFill2 = (op) => {
1397
+ return op.op === "FillPath" && op.isShadow === true;
1398
+ };
1399
+ var isGlyphFill2 = (op) => {
1400
+ return op.op === "FillPath" && op.isShadow !== true;
1401
+ };
1402
+
1197
1403
  // src/env/entry.web.ts
1404
+ var DEFAULT_ROBOTO_URL = "https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu4mxP.ttf";
1198
1405
  async function createTextEngine(opts = {}) {
1199
1406
  const width = opts.width ?? CANVAS_CONFIG.DEFAULTS.width;
1200
1407
  const height = opts.height ?? CANVAS_CONFIG.DEFAULTS.height;
@@ -1202,109 +1409,237 @@ async function createTextEngine(opts = {}) {
1202
1409
  const wasmBaseURL = opts.wasmBaseURL;
1203
1410
  const fonts = new FontRegistry(wasmBaseURL);
1204
1411
  const layout = new LayoutEngine(fonts);
1205
- await fonts.init();
1412
+ try {
1413
+ FontRegistry.setFallbackLoader(async (desc) => {
1414
+ const family = (desc.family ?? "Roboto").toLowerCase();
1415
+ const weight = `${desc.weight ?? "400"}`;
1416
+ const style = desc.style ?? "normal";
1417
+ if (family === "roboto" && weight === "400" && style === "normal") {
1418
+ return fetchToArrayBuffer(DEFAULT_ROBOTO_URL);
1419
+ }
1420
+ return void 0;
1421
+ });
1422
+ await fonts.init();
1423
+ } catch (err) {
1424
+ throw new Error(
1425
+ `Failed to initialize font registry: ${err instanceof Error ? err.message : String(err)}`
1426
+ );
1427
+ }
1206
1428
  async function ensureFonts(asset) {
1207
- if (asset.customFonts) {
1208
- for (const cf of asset.customFonts) {
1209
- const bytes = await fetchToArrayBuffer(cf.src);
1210
- await fonts.registerFromBytes(bytes, {
1211
- family: cf.family,
1212
- weight: cf.weight ?? "400",
1213
- style: cf.style ?? "normal"
1214
- });
1429
+ try {
1430
+ if (asset.customFonts) {
1431
+ for (const cf of asset.customFonts) {
1432
+ try {
1433
+ const bytes = await fetchToArrayBuffer(cf.src);
1434
+ await fonts.registerFromBytes(bytes, {
1435
+ family: cf.family,
1436
+ weight: cf.weight ?? "400",
1437
+ style: cf.style ?? "normal"
1438
+ });
1439
+ } catch (err) {
1440
+ throw new Error(
1441
+ `Failed to load custom font "${cf.family}" from ${cf.src}: ${err instanceof Error ? err.message : String(err)}`
1442
+ );
1443
+ }
1444
+ }
1215
1445
  }
1446
+ const main = asset.font ?? {
1447
+ family: "Roboto",
1448
+ weight: "400",
1449
+ style: "normal",
1450
+ size: 48,
1451
+ color: "#000000",
1452
+ opacity: 1
1453
+ };
1454
+ const desc = { family: main.family, weight: `${main.weight}`, style: main.style };
1455
+ const ensureFace = async () => {
1456
+ try {
1457
+ await fonts.getFace(desc);
1458
+ } catch {
1459
+ const wantsDefaultRoboto = (main.family || "Roboto").toLowerCase() === "roboto" && `${main.weight}` === "400" && main.style === "normal";
1460
+ if (wantsDefaultRoboto) {
1461
+ const bytes = await fetchToArrayBuffer(DEFAULT_ROBOTO_URL);
1462
+ await fonts.registerFromBytes(bytes, {
1463
+ family: "Roboto",
1464
+ weight: "400",
1465
+ style: "normal"
1466
+ });
1467
+ } else {
1468
+ throw new Error(
1469
+ `Font not registered for ${desc.family}__${desc.weight}__${desc.style}`
1470
+ );
1471
+ }
1472
+ }
1473
+ };
1474
+ await ensureFace();
1475
+ return main;
1476
+ } catch (err) {
1477
+ if (err instanceof Error) throw err;
1478
+ throw new Error(`Failed to ensure fonts: ${String(err)}`);
1216
1479
  }
1217
- const main = asset.font ?? {
1218
- family: "Roboto",
1219
- weight: "400",
1220
- style: "normal",
1221
- size: 48,
1222
- color: "#000000",
1223
- opacity: 1
1224
- };
1225
- return main;
1226
1480
  }
1227
1481
  return {
1228
1482
  validate(input) {
1229
- const { value, error } = RichTextAssetSchema.validate(input, {
1230
- abortEarly: false,
1231
- convert: true
1232
- });
1233
- if (error) throw error;
1234
- return { value };
1483
+ try {
1484
+ const { value, error } = RichTextAssetSchema.validate(input, {
1485
+ abortEarly: false,
1486
+ convert: true
1487
+ });
1488
+ if (error) throw error;
1489
+ return { value };
1490
+ } catch (err) {
1491
+ if (err instanceof Error) {
1492
+ throw new Error(`Validation failed: ${err.message}`);
1493
+ }
1494
+ throw new Error(`Validation failed: ${String(err)}`);
1495
+ }
1235
1496
  },
1236
1497
  async registerFontFromUrl(url, desc) {
1237
- const bytes = await fetchToArrayBuffer(url);
1238
- await fonts.registerFromBytes(bytes, desc);
1498
+ try {
1499
+ const bytes = await fetchToArrayBuffer(url);
1500
+ await fonts.registerFromBytes(bytes, desc);
1501
+ } catch (err) {
1502
+ throw new Error(
1503
+ `Failed to register font "${desc.family}" from URL ${url}: ${err instanceof Error ? err.message : String(err)}`
1504
+ );
1505
+ }
1239
1506
  },
1240
1507
  async registerFontFromFile(source, desc) {
1241
- let bytes;
1242
- if (typeof source === "string") {
1243
- bytes = await fetchToArrayBuffer(source);
1244
- } else {
1245
- bytes = await source.arrayBuffer();
1508
+ try {
1509
+ let bytes;
1510
+ if (typeof source === "string") {
1511
+ try {
1512
+ bytes = await fetchToArrayBuffer(source);
1513
+ } catch (err) {
1514
+ throw new Error(
1515
+ `Failed to fetch font from ${source}: ${err instanceof Error ? err.message : String(err)}`
1516
+ );
1517
+ }
1518
+ } else {
1519
+ try {
1520
+ bytes = await source.arrayBuffer();
1521
+ } catch (err) {
1522
+ throw new Error(
1523
+ `Failed to read Blob as ArrayBuffer: ${err instanceof Error ? err.message : String(err)}`
1524
+ );
1525
+ }
1526
+ }
1527
+ await fonts.registerFromBytes(bytes, desc);
1528
+ } catch (err) {
1529
+ if (err instanceof Error) {
1530
+ throw err;
1531
+ }
1532
+ throw new Error(
1533
+ `Failed to register font "${desc.family}" from ${typeof source === "string" ? source : "Blob"}: ${String(err)}`
1534
+ );
1246
1535
  }
1247
- await fonts.registerFromBytes(bytes, desc);
1248
1536
  },
1249
1537
  async renderFrame(asset, tSeconds) {
1250
- const main = await ensureFonts(asset);
1251
- const desc = { family: main.family, weight: `${main.weight}`, style: main.style };
1252
- const lines = await layout.layout({
1253
- text: asset.text,
1254
- width: asset.width ?? width,
1255
- letterSpacing: asset.style?.letterSpacing ?? 0,
1256
- fontSize: main.size,
1257
- lineHeight: asset.style?.lineHeight ?? 1.2,
1258
- desc,
1259
- textTransform: asset.style?.textTransform ?? "none"
1260
- });
1261
- const textRect = { x: 0, y: 0, width: asset.width ?? width, height: asset.height ?? height };
1262
- const ops0 = await buildDrawOps({
1263
- canvas: { width, height, pixelRatio },
1264
- textRect,
1265
- lines,
1266
- font: {
1267
- family: main.family,
1268
- size: main.size,
1269
- weight: `${main.weight}`,
1270
- style: main.style,
1271
- color: main.color,
1272
- opacity: main.opacity
1273
- },
1274
- style: {
1275
- lineHeight: asset.style?.lineHeight ?? 1.2,
1276
- textDecoration: asset.style?.textDecoration ?? "none",
1277
- gradient: asset.style?.gradient
1278
- },
1279
- stroke: asset.stroke,
1280
- shadow: asset.shadow,
1281
- align: asset.align ?? { horizontal: "left", vertical: "middle" },
1282
- background: asset.background,
1283
- glyphPathProvider: (gid) => fonts.glyphPath(desc, gid),
1284
- getUnitsPerEm: () => fonts.getUnitsPerEm(desc)
1285
- });
1286
- const ops = applyAnimation(ops0, lines, {
1287
- t: tSeconds,
1288
- fontSize: main.size,
1289
- anim: asset.animation ? {
1290
- preset: asset.animation.preset,
1291
- speed: asset.animation.speed,
1292
- duration: asset.animation.duration,
1293
- style: asset.animation.style,
1294
- direction: asset.animation.direction
1295
- } : void 0
1296
- });
1297
- return ops;
1538
+ try {
1539
+ const main = await ensureFonts(asset);
1540
+ const desc = { family: main.family, weight: `${main.weight}`, style: main.style };
1541
+ let lines;
1542
+ try {
1543
+ lines = await layout.layout({
1544
+ text: asset.text,
1545
+ width: asset.width ?? width,
1546
+ letterSpacing: asset.style?.letterSpacing ?? 0,
1547
+ fontSize: main.size,
1548
+ lineHeight: asset.style?.lineHeight ?? 1.2,
1549
+ desc,
1550
+ textTransform: asset.style?.textTransform ?? "none"
1551
+ });
1552
+ } catch (err) {
1553
+ throw new Error(
1554
+ `Failed to layout text: ${err instanceof Error ? err.message : String(err)}`
1555
+ );
1556
+ }
1557
+ const textRect = {
1558
+ x: 0,
1559
+ y: 0,
1560
+ width: asset.width ?? width,
1561
+ height: asset.height ?? height
1562
+ };
1563
+ const canvasW = asset.width ?? width;
1564
+ const canvasH = asset.height ?? height;
1565
+ const canvasPR = asset.pixelRatio ?? pixelRatio;
1566
+ let ops0;
1567
+ try {
1568
+ ops0 = await buildDrawOps({
1569
+ canvas: { width: canvasW, height: canvasH, pixelRatio: canvasPR },
1570
+ textRect,
1571
+ lines,
1572
+ font: {
1573
+ family: main.family,
1574
+ size: main.size,
1575
+ weight: `${main.weight}`,
1576
+ style: main.style,
1577
+ color: main.color,
1578
+ opacity: main.opacity
1579
+ },
1580
+ style: {
1581
+ lineHeight: asset.style?.lineHeight ?? 1.2,
1582
+ textDecoration: asset.style?.textDecoration ?? "none",
1583
+ gradient: asset.style?.gradient
1584
+ },
1585
+ stroke: asset.stroke,
1586
+ shadow: asset.shadow,
1587
+ align: asset.align ?? { horizontal: "center", vertical: "middle" },
1588
+ background: asset.background,
1589
+ glyphPathProvider: (gid) => fonts.glyphPath(desc, gid),
1590
+ getUnitsPerEm: () => fonts.getUnitsPerEm(desc)
1591
+ });
1592
+ } catch (err) {
1593
+ throw new Error(
1594
+ `Failed to build draw operations: ${err instanceof Error ? err.message : String(err)}`
1595
+ );
1596
+ }
1597
+ try {
1598
+ const ops = applyAnimation(ops0, lines, {
1599
+ t: tSeconds,
1600
+ fontSize: main.size,
1601
+ anim: asset.animation ? {
1602
+ preset: asset.animation.preset,
1603
+ speed: asset.animation.speed,
1604
+ duration: asset.animation.duration,
1605
+ style: asset.animation.style,
1606
+ direction: asset.animation.direction
1607
+ } : void 0
1608
+ });
1609
+ return ops;
1610
+ } catch (err) {
1611
+ throw new Error(
1612
+ `Failed to apply animation: ${err instanceof Error ? err.message : String(err)}`
1613
+ );
1614
+ }
1615
+ } catch (err) {
1616
+ if (err instanceof Error) {
1617
+ throw err;
1618
+ }
1619
+ throw new Error(`Failed to render frame at time ${tSeconds}s: ${String(err)}`);
1620
+ }
1298
1621
  },
1299
1622
  createRenderer(canvas) {
1300
- return createWebPainter(canvas);
1623
+ try {
1624
+ return createWebPainter(canvas);
1625
+ } catch (err) {
1626
+ throw new Error(
1627
+ `Failed to create renderer: ${err instanceof Error ? err.message : String(err)}`
1628
+ );
1629
+ }
1301
1630
  },
1302
1631
  destroy() {
1303
- fonts.destroy();
1632
+ try {
1633
+ fonts.destroy();
1634
+ } catch (err) {
1635
+ console.error(`Error during cleanup: ${err instanceof Error ? err.message : String(err)}`);
1636
+ }
1304
1637
  }
1305
1638
  };
1306
1639
  }
1307
1640
  export {
1308
- createTextEngine
1641
+ createTextEngine,
1642
+ isGlyphFill2 as isGlyphFill,
1643
+ isShadowFill2 as isShadowFill
1309
1644
  };
1310
1645
  //# sourceMappingURL=entry.web.js.map