@fieldnotes/core 0.50.7 → 0.51.0

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/index.cjs CHANGED
@@ -239,6 +239,17 @@ function sanitizeAttributes(el, tag) {
239
239
 
240
240
  // src/core/state-serializer.ts
241
241
  var CURRENT_VERSION = 2;
242
+ var ELEMENT_TYPES = [
243
+ "stroke",
244
+ "note",
245
+ "arrow",
246
+ "image",
247
+ "html",
248
+ "text",
249
+ "shape",
250
+ "grid",
251
+ "template"
252
+ ];
242
253
  function exportState(elements, camera, layers = [], activeLayerId) {
243
254
  const state = {
244
255
  version: CURRENT_VERSION,
@@ -264,37 +275,42 @@ function parseState(json) {
264
275
  return data;
265
276
  }
266
277
  function validateState(data) {
267
- if (!data || typeof data !== "object") {
278
+ if (!isRecord(data)) {
268
279
  throw new Error("Invalid state: expected an object");
269
280
  }
270
281
  const obj = data;
271
- if (typeof obj["version"] !== "number") {
282
+ if (!Number.isInteger(obj["version"]) || obj["version"] < 1) {
272
283
  throw new Error("Invalid state: missing or invalid version");
273
284
  }
274
- if (!obj["camera"] || typeof obj["camera"] !== "object") {
285
+ if (obj["version"] > CURRENT_VERSION) {
286
+ throw new Error(`Invalid state: unsupported version ${String(obj["version"])}`);
287
+ }
288
+ if (!isRecord(obj["camera"])) {
275
289
  throw new Error("Invalid state: missing camera");
276
290
  }
277
291
  const cam = obj["camera"];
278
- if (!cam["position"] || typeof cam["position"] !== "object") {
292
+ if (!isRecord(cam["position"])) {
279
293
  throw new Error("Invalid state: missing camera.position");
280
294
  }
281
- const pos = cam["position"];
282
- if (typeof pos["x"] !== "number" || typeof pos["y"] !== "number") {
283
- throw new Error("Invalid state: camera.position must have x and y numbers");
295
+ if (!isPoint(cam["position"])) {
296
+ throw new Error("Invalid state: camera.position must have finite x and y numbers");
284
297
  }
285
- if (typeof cam["zoom"] !== "number") {
286
- throw new Error("Invalid state: missing camera.zoom");
298
+ if (!isFiniteNumber(cam["zoom"]) || cam["zoom"] <= 0) {
299
+ throw new Error("Invalid state: camera.zoom must be a positive finite number");
287
300
  }
288
301
  if (!Array.isArray(obj["elements"])) {
289
302
  throw new Error("Invalid state: elements must be an array");
290
303
  }
291
- for (const el of obj["elements"]) {
292
- validateElement(el);
293
- migrateElement(el);
304
+ if (obj["layers"] !== void 0 && !Array.isArray(obj["layers"])) {
305
+ throw new Error("Invalid state: layers must be an array");
294
306
  }
295
- cleanBindings(obj["elements"]);
296
- const layers = obj["layers"];
297
- if (!Array.isArray(layers) || layers.length === 0) {
307
+ const elements = obj["elements"];
308
+ const hasLayers = Array.isArray(obj["layers"]) && obj["layers"].length > 0;
309
+ for (const el of elements) {
310
+ if (!isRecord(el)) throw new Error("Invalid element: expected an object");
311
+ migrateElement(el, !hasLayers);
312
+ }
313
+ if (!hasLayers) {
298
314
  obj["layers"] = [
299
315
  {
300
316
  id: "default-layer",
@@ -306,31 +322,73 @@ function validateState(data) {
306
322
  }
307
323
  ];
308
324
  }
325
+ const layers = obj["layers"];
326
+ const layerIds = /* @__PURE__ */ new Set();
327
+ for (const layer of layers) {
328
+ validateLayer(layer);
329
+ if (layerIds.has(layer.id)) throw new Error(`Invalid state: duplicate layer id "${layer.id}"`);
330
+ layerIds.add(layer.id);
331
+ }
332
+ const elementIds = /* @__PURE__ */ new Set();
333
+ for (const el of elements) {
334
+ validateElement(el);
335
+ if (elementIds.has(el.id)) throw new Error(`Invalid state: duplicate element id "${el.id}"`);
336
+ elementIds.add(el.id);
337
+ if (!layerIds.has(el.layerId)) {
338
+ throw new Error(`Invalid element "${el.id}": unknown layerId "${el.layerId}"`);
339
+ }
340
+ }
341
+ if (obj["activeLayerId"] !== void 0) {
342
+ if (typeof obj["activeLayerId"] !== "string" || !layerIds.has(obj["activeLayerId"])) {
343
+ throw new Error("Invalid state: activeLayerId must reference an existing layer");
344
+ }
345
+ }
346
+ cleanBindings(elements);
309
347
  }
310
- var VALID_TYPES = /* @__PURE__ */ new Set([
311
- "stroke",
312
- "note",
313
- "arrow",
314
- "image",
315
- "html",
316
- "text",
317
- "shape",
318
- "grid",
319
- "template"
320
- ]);
321
348
  function validateElement(el) {
322
- if (!el || typeof el !== "object") {
349
+ if (!isRecord(el)) {
323
350
  throw new Error("Invalid element: expected an object");
324
351
  }
325
- const obj = el;
326
- if (typeof obj["id"] !== "string") {
352
+ if (typeof el["id"] !== "string" || el["id"].length === 0) {
327
353
  throw new Error("Invalid element: missing id");
328
354
  }
329
- if (typeof obj["type"] !== "string" || !VALID_TYPES.has(obj["type"])) {
330
- throw new Error(`Invalid element: unknown type "${String(obj["type"])}"`);
355
+ if (!isEnum(el["type"], ELEMENT_TYPES)) {
356
+ throw new Error(`Invalid element: unknown type "${String(el["type"])}"`);
357
+ }
358
+ if (!isFiniteNumber(el["zIndex"])) {
359
+ throw new Error(`Invalid element "${el["id"]}": missing or invalid zIndex`);
360
+ }
361
+ if (!isPoint(el["position"]) || typeof el["locked"] !== "boolean" || typeof el["layerId"] !== "string" || !isOptional(el["groupId"], isString) || !isOptional(el["rotation"], isFiniteNumber)) {
362
+ throw new Error(`Invalid element "${el["id"]}": invalid base fields or geometry`);
363
+ }
364
+ const valid = validateTypeFields(el, el["type"]);
365
+ if (!valid) throw new Error(`Invalid element "${el["id"]}": malformed ${el["type"]} data`);
366
+ }
367
+ function validateTypeFields(el, type) {
368
+ switch (type) {
369
+ case "stroke":
370
+ return Array.isArray(el["points"]) && el["points"].every(isStrokePoint) && isString(el["color"]) && isFiniteNumber(el["width"]) && isFiniteNumber(el["opacity"]) && isOptionalEnum(el["blendMode"], ["multiply"]);
371
+ case "note":
372
+ return isSize(el["size"]) && isString(el["text"]) && isString(el["backgroundColor"]) && isString(el["textColor"]) && isOptional(el["fontSize"], isFiniteNumber);
373
+ case "arrow":
374
+ return isPoint(el["from"]) && isPoint(el["to"]) && isFiniteNumber(el["bend"]) && isString(el["color"]) && isFiniteNumber(el["width"]) && isOptional(el["fromBinding"], isBinding) && isOptional(el["toBinding"], isBinding) && isOptional(el["cachedControlPoint"], isPoint) && isOptional(el["label"], isString) && isOptionalEnum(el["strokeStyle"], ["solid", "dashed", "dotted"]);
375
+ case "image":
376
+ return isSize(el["size"]) && isString(el["src"]);
377
+ case "html":
378
+ return isSize(el["size"]) && isOptional(el["domId"], isString) && isOptional(el["interactive"], isBoolean) && isOptional(el["htmlType"], isString) && isOptional(el["data"], isRecord);
379
+ case "text":
380
+ return isSize(el["size"]) && isString(el["text"]) && isFiniteNumber(el["fontSize"]) && isString(el["color"]) && isEnum(el["textAlign"], ["left", "center", "right"]);
381
+ case "shape":
382
+ return isEnum(el["shape"], ["rectangle", "ellipse", "line"]) && isSize(el["size"]) && isString(el["strokeColor"]) && isFiniteNumber(el["strokeWidth"]) && isString(el["fillColor"]) && isOptional(el["flip"], isBoolean);
383
+ case "grid":
384
+ return isEnum(el["gridType"], ["square", "hex"]) && isEnum(el["hexOrientation"], ["pointy", "flat"]) && isFiniteNumber(el["cellSize"]) && isString(el["strokeColor"]) && isFiniteNumber(el["strokeWidth"]) && isFiniteNumber(el["opacity"]);
385
+ case "template":
386
+ return isEnum(el["templateShape"], ["circle", "cone", "line", "square", "rectangle"]) && isFiniteNumber(el["radius"]) && isFiniteNumber(el["angle"]) && isOptional(el["width"], isFiniteNumber) && isString(el["fillColor"]) && isString(el["strokeColor"]) && isFiniteNumber(el["strokeWidth"]) && isFiniteNumber(el["opacity"]) && isOptional(el["feetPerCell"], isFiniteNumber) && isOptional(el["radiusFeet"], isFiniteNumber) && isOptionalEnum(el["renderStyle"], ["cells", "geometric"]);
331
387
  }
332
- if (typeof obj["zIndex"] !== "number") {
333
- throw new Error("Invalid element: missing zIndex");
388
+ }
389
+ function validateLayer(layer) {
390
+ if (!isRecord(layer) || typeof layer["id"] !== "string" || layer["id"].length === 0 || typeof layer["name"] !== "string" || typeof layer["visible"] !== "boolean" || typeof layer["locked"] !== "boolean" || !isFiniteNumber(layer["order"]) || !isFiniteNumber(layer["opacity"]) || layer["opacity"] < 0 || layer["opacity"] > 1) {
391
+ throw new Error("Invalid state: malformed layer");
334
392
  }
335
393
  }
336
394
  function cleanBindings(elements) {
@@ -347,30 +405,63 @@ function cleanBindings(elements) {
347
405
  }
348
406
  }
349
407
  }
350
- function migrateElement(obj) {
351
- if (typeof obj["layerId"] !== "string") {
408
+ function migrateElement(obj, useDefaultLayer) {
409
+ if (obj["layerId"] === void 0 || useDefaultLayer && obj["layerId"] === "") {
352
410
  obj["layerId"] = "default-layer";
353
411
  }
354
- if (obj["type"] === "arrow" && typeof obj["bend"] !== "number") {
412
+ if (obj["type"] === "arrow" && obj["bend"] === void 0) {
355
413
  obj["bend"] = 0;
356
414
  }
357
415
  if (obj["type"] === "stroke" && Array.isArray(obj["points"])) {
358
416
  for (const pt of obj["points"]) {
359
- if (typeof pt["pressure"] !== "number") {
417
+ if (pt["pressure"] === void 0) {
360
418
  pt["pressure"] = 0.5;
361
419
  }
362
420
  }
363
421
  }
364
- if (obj["type"] === "shape" && typeof obj["shape"] !== "string") {
422
+ if (obj["type"] === "shape" && obj["shape"] === void 0) {
365
423
  obj["shape"] = "rectangle";
366
424
  }
367
- if (obj["type"] === "note" && typeof obj["textColor"] !== "string") {
425
+ if (obj["type"] === "note" && obj["textColor"] === void 0) {
368
426
  obj["textColor"] = "#000000";
369
427
  }
370
428
  if ((obj["type"] === "note" || obj["type"] === "text") && typeof obj["text"] === "string") {
371
429
  obj["text"] = sanitizeNoteHtml(obj["text"]);
372
430
  }
373
431
  }
432
+ function isRecord(value) {
433
+ return typeof value === "object" && value !== null && !Array.isArray(value);
434
+ }
435
+ function isFiniteNumber(value) {
436
+ return typeof value === "number" && Number.isFinite(value);
437
+ }
438
+ function isString(value) {
439
+ return typeof value === "string";
440
+ }
441
+ function isBoolean(value) {
442
+ return typeof value === "boolean";
443
+ }
444
+ function isOptional(value, validate) {
445
+ return value === void 0 || validate(value);
446
+ }
447
+ function isEnum(value, values) {
448
+ return typeof value === "string" && values.includes(value);
449
+ }
450
+ function isOptionalEnum(value, values) {
451
+ return value === void 0 || isEnum(value, values);
452
+ }
453
+ function isPoint(value) {
454
+ return isRecord(value) && isFiniteNumber(value["x"]) && isFiniteNumber(value["y"]);
455
+ }
456
+ function isSize(value) {
457
+ return isRecord(value) && isFiniteNumber(value["w"]) && isFiniteNumber(value["h"]);
458
+ }
459
+ function isStrokePoint(value) {
460
+ return isRecord(value) && isPoint(value) && isFiniteNumber(value["pressure"]);
461
+ }
462
+ function isBinding(value) {
463
+ return isRecord(value) && typeof value["elementId"] === "string";
464
+ }
374
465
 
375
466
  // src/core/storage/local-storage-adapter.ts
376
467
  var LocalStorageAdapter = class {
@@ -5352,6 +5443,9 @@ function renderTextOnCanvas(ctx, text) {
5352
5443
  }
5353
5444
 
5354
5445
  // src/canvas/export-image.ts
5446
+ var DEFAULT_IMAGE_TIMEOUT_MS = 1e4;
5447
+ var DEFAULT_MAX_DIMENSION = 16384;
5448
+ var DEFAULT_MAX_PIXELS = 67108864;
5355
5449
  var center = (b) => ({ x: b.x + b.w / 2, y: b.y + b.h / 2 });
5356
5450
  function getStrokeBounds(el) {
5357
5451
  if (el.type !== "stroke") return null;
@@ -5461,12 +5555,53 @@ function renderGridForBounds(ctx, grid, bounds) {
5461
5555
  );
5462
5556
  }
5463
5557
  }
5464
- function loadImages(elements) {
5558
+ function positiveOption(value, fallback, name) {
5559
+ const resolved = value ?? fallback;
5560
+ if (!Number.isFinite(resolved) || resolved <= 0) {
5561
+ throw new RangeError(`${name} must be a finite number greater than 0`);
5562
+ }
5563
+ return resolved;
5564
+ }
5565
+ function nonNegativeOption(value, fallback, name) {
5566
+ const resolved = value ?? fallback;
5567
+ if (!Number.isFinite(resolved) || resolved < 0) {
5568
+ throw new RangeError(`${name} must be a finite number greater than or equal to 0`);
5569
+ }
5570
+ return resolved;
5571
+ }
5572
+ function assertExportSize(width, height, options) {
5573
+ const maxDimension = positiveOption(options.maxDimension, DEFAULT_MAX_DIMENSION, "maxDimension");
5574
+ const maxPixels = positiveOption(options.maxPixels, DEFAULT_MAX_PIXELS, "maxPixels");
5575
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
5576
+ throw new RangeError("Export dimensions must be finite numbers greater than 0");
5577
+ }
5578
+ if (width > maxDimension || height > maxDimension) {
5579
+ throw new RangeError(
5580
+ `Export dimensions ${width}x${height} exceed the maximum dimension of ${maxDimension}`
5581
+ );
5582
+ }
5583
+ if (width * height > maxPixels) {
5584
+ throw new RangeError(
5585
+ `Export size ${width}x${height} exceeds the maximum of ${maxPixels} pixels`
5586
+ );
5587
+ }
5588
+ }
5589
+ function validateExportResourceOptions(options) {
5590
+ positiveOption(options.imageTimeoutMs, DEFAULT_IMAGE_TIMEOUT_MS, "imageTimeoutMs");
5591
+ positiveOption(options.maxDimension, DEFAULT_MAX_DIMENSION, "maxDimension");
5592
+ positiveOption(options.maxPixels, DEFAULT_MAX_PIXELS, "maxPixels");
5593
+ }
5594
+ function loadImages(elements, options = {}) {
5465
5595
  const imageElements = elements.filter(
5466
5596
  (el) => el.type === "image" && "src" in el
5467
5597
  );
5468
5598
  const cache3 = /* @__PURE__ */ new Map();
5469
5599
  if (imageElements.length === 0) return Promise.resolve(cache3);
5600
+ const timeoutMs = positiveOption(
5601
+ options.imageTimeoutMs,
5602
+ DEFAULT_IMAGE_TIMEOUT_MS,
5603
+ "imageTimeoutMs"
5604
+ );
5470
5605
  return new Promise((resolve) => {
5471
5606
  let remaining = imageElements.length;
5472
5607
  const done = () => {
@@ -5476,19 +5611,41 @@ function loadImages(elements) {
5476
5611
  for (const el of imageElements) {
5477
5612
  const img = new Image();
5478
5613
  img.crossOrigin = "anonymous";
5614
+ let settled = false;
5615
+ const timer = setTimeout(() => {
5616
+ if (settled) return;
5617
+ settled = true;
5618
+ img.onload = null;
5619
+ img.onerror = null;
5620
+ options.onAssetError?.({ elementId: el.id, src: el.src, reason: "timeout" });
5621
+ done();
5622
+ }, timeoutMs);
5623
+ const settle = () => {
5624
+ if (settled) return false;
5625
+ settled = true;
5626
+ clearTimeout(timer);
5627
+ img.onload = null;
5628
+ img.onerror = null;
5629
+ return true;
5630
+ };
5479
5631
  img.onload = () => {
5632
+ if (!settle()) return;
5480
5633
  cache3.set(el.id, img);
5481
5634
  done();
5482
5635
  };
5483
- img.onerror = done;
5484
- const sep = el.src.includes("?") ? "&" : "?";
5485
- img.src = `${el.src}${sep}_cors=1`;
5636
+ img.onerror = (cause) => {
5637
+ if (!settle()) return;
5638
+ options.onAssetError?.({ elementId: el.id, src: el.src, reason: "load", cause });
5639
+ done();
5640
+ };
5641
+ img.src = el.src;
5486
5642
  }
5487
5643
  });
5488
5644
  }
5489
5645
  async function exportImage(store, options = {}, layerManager) {
5490
- const scale = options.scale ?? 2;
5491
- const padding = options.padding ?? 0;
5646
+ const scale = positiveOption(options.scale, 2, "scale");
5647
+ const padding = nonNegativeOption(options.padding, 0, "padding");
5648
+ validateExportResourceOptions(options);
5492
5649
  const background = options.background ?? "#ffffff";
5493
5650
  const filter = options.filter;
5494
5651
  const allElements = store.getAll();
@@ -5498,10 +5655,13 @@ async function exportImage(store, options = {}, layerManager) {
5498
5655
  }
5499
5656
  const bounds = computeBounds(visibleElements, padding);
5500
5657
  if (!bounds) return null;
5501
- const imageCache = await loadImages(visibleElements);
5658
+ const width = Math.ceil(bounds.w * scale);
5659
+ const height = Math.ceil(bounds.h * scale);
5660
+ assertExportSize(width, height, options);
5661
+ const imageCache = await loadImages(visibleElements, options);
5502
5662
  const canvas = document.createElement("canvas");
5503
- canvas.width = Math.ceil(bounds.w * scale);
5504
- canvas.height = Math.ceil(bounds.h * scale);
5663
+ canvas.width = width;
5664
+ canvas.height = height;
5505
5665
  const ctx = canvas.getContext("2d");
5506
5666
  if (!ctx) return null;
5507
5667
  ctx.scale(scale, scale);
@@ -5684,14 +5844,17 @@ function emitImage(image, dataUri) {
5684
5844
  const { w, h } = image.size;
5685
5845
  return `<image href="${esc(href)}" x="${n(x)}" y="${n(y)}" width="${n(w)}" height="${n(h)}" />`;
5686
5846
  }
5687
- function emitText(text, rasterScale) {
5847
+ function emitText(text, rasterScale, resourceOptions) {
5688
5848
  if (!text.text) return "";
5689
5849
  const { x, y } = text.position;
5690
5850
  const { w, h } = text.size;
5691
5851
  if (typeof document === "undefined") return "";
5852
+ const width = Math.max(1, Math.ceil(w * rasterScale));
5853
+ const height = Math.max(1, Math.ceil(h * rasterScale));
5854
+ assertExportSize(width, height, resourceOptions);
5692
5855
  const canvas = document.createElement("canvas");
5693
- canvas.width = Math.max(1, Math.ceil(w * rasterScale));
5694
- canvas.height = Math.max(1, Math.ceil(h * rasterScale));
5856
+ canvas.width = width;
5857
+ canvas.height = height;
5695
5858
  const ctx = canvas.getContext("2d");
5696
5859
  if (!ctx) return "";
5697
5860
  ctx.scale(rasterScale, rasterScale);
@@ -5706,13 +5869,16 @@ function emitText(text, rasterScale) {
5706
5869
  if (!dataUri || !dataUri.startsWith("data:")) return "";
5707
5870
  return `<image href="${esc(dataUri)}" x="${n(x)}" y="${n(y)}" width="${n(w)}" height="${n(h)}" />`;
5708
5871
  }
5709
- function emitNote(note, rasterScale) {
5872
+ function emitNote(note, rasterScale, resourceOptions) {
5710
5873
  const { x, y } = note.position;
5711
5874
  const { w, h } = note.size;
5712
5875
  if (typeof document === "undefined") return emitNotePlaceholder(note);
5876
+ const width = Math.max(1, Math.ceil(w * rasterScale));
5877
+ const height = Math.max(1, Math.ceil(h * rasterScale));
5878
+ assertExportSize(width, height, resourceOptions);
5713
5879
  const canvas = document.createElement("canvas");
5714
- canvas.width = Math.max(1, Math.ceil(w * rasterScale));
5715
- canvas.height = Math.max(1, Math.ceil(h * rasterScale));
5880
+ canvas.width = width;
5881
+ canvas.height = height;
5716
5882
  const ctx = canvas.getContext("2d");
5717
5883
  if (!ctx) return emitNotePlaceholder(note);
5718
5884
  ctx.scale(rasterScale, rasterScale);
@@ -5873,8 +6039,9 @@ function emitHexTemplate(t, grid) {
5873
6039
  return `<path d="${d}" fill="${esc(t.fillColor)}" stroke="${esc(t.strokeColor)}" stroke-width="${n(t.strokeWidth)}" opacity="${n(t.opacity)}" />`;
5874
6040
  }
5875
6041
  async function exportSvg(store, options = {}, layerManager) {
5876
- const padding = options.padding ?? 0;
5877
- const rasterScale = options.rasterScale ?? 2;
6042
+ const padding = nonNegativeOption(options.padding, 0, "padding");
6043
+ const rasterScale = positiveOption(options.rasterScale, 2, "rasterScale");
6044
+ validateExportResourceOptions(options);
5878
6045
  const filter = options.filter;
5879
6046
  const allElements = store.getAll();
5880
6047
  let visibleElements = layerManager ? allElements.filter((el) => layerManager.isLayerVisible(el.layerId)) : allElements;
@@ -5883,11 +6050,12 @@ async function exportSvg(store, options = {}, layerManager) {
5883
6050
  if (!bounds) {
5884
6051
  return `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>`;
5885
6052
  }
6053
+ assertExportSize(Math.ceil(bounds.w), Math.ceil(bounds.h), options);
5886
6054
  const remoteImages = visibleElements.filter(
5887
6055
  (el) => el.type === "image" && !el.src.startsWith("data:")
5888
6056
  );
5889
- const imageCache = await loadImages(remoteImages);
5890
- const imageDataUris = encodeImages(visibleElements, imageCache, rasterScale);
6057
+ const imageCache = await loadImages(remoteImages, options);
6058
+ const imageDataUris = encodeImages(visibleElements, imageCache, rasterScale, options);
5891
6059
  const grids = visibleElements.filter((el) => el.type === "grid");
5892
6060
  const firstGrid = grids[0];
5893
6061
  let body = "";
@@ -5896,7 +6064,7 @@ async function exportSvg(store, options = {}, layerManager) {
5896
6064
  }
5897
6065
  const layerBodies = /* @__PURE__ */ new Map();
5898
6066
  for (const el of visibleElements) {
5899
- const emitted = emitElement(el, imageDataUris, rasterScale, firstGrid, store);
6067
+ const emitted = emitElement(el, imageDataUris, rasterScale, firstGrid, store, options);
5900
6068
  layerBodies.set(el.layerId, (layerBodies.get(el.layerId) ?? "") + emitted);
5901
6069
  }
5902
6070
  for (const [layerId, emitted] of layerBodies) {
@@ -5910,7 +6078,7 @@ async function exportSvg(store, options = {}, layerManager) {
5910
6078
  }
5911
6079
  return `<svg xmlns="http://www.w3.org/2000/svg" width="${n(bounds.w)}" height="${n(bounds.h)}" viewBox="${n(bounds.x)} ${n(bounds.y)} ${n(bounds.w)} ${n(bounds.h)}">${body}</svg>`;
5912
6080
  }
5913
- function emitElement(el, imageDataUris, rasterScale, firstGrid, store) {
6081
+ function emitElement(el, imageDataUris, rasterScale, firstGrid, store, resourceOptions) {
5914
6082
  switch (el.type) {
5915
6083
  case "stroke":
5916
6084
  return withRotationSvg(el, emitStroke(el));
@@ -5921,9 +6089,9 @@ function emitElement(el, imageDataUris, rasterScale, firstGrid, store) {
5921
6089
  case "image":
5922
6090
  return withRotationSvg(el, emitImage(el, imageDataUris.get(el.id)));
5923
6091
  case "text":
5924
- return withRotationSvg(el, emitText(el, rasterScale));
6092
+ return withRotationSvg(el, emitText(el, rasterScale, resourceOptions));
5925
6093
  case "note":
5926
- return withRotationSvg(el, emitNote(el, rasterScale));
6094
+ return withRotationSvg(el, emitNote(el, rasterScale, resourceOptions));
5927
6095
  case "template":
5928
6096
  return emitTemplate(el, firstGrid);
5929
6097
  case "grid":
@@ -5934,7 +6102,7 @@ function emitElement(el, imageDataUris, rasterScale, firstGrid, store) {
5934
6102
  return "";
5935
6103
  }
5936
6104
  }
5937
- function encodeImages(elements, imageCache, rasterScale) {
6105
+ function encodeImages(elements, imageCache, rasterScale, resourceOptions) {
5938
6106
  const out = /* @__PURE__ */ new Map();
5939
6107
  for (const el of elements) {
5940
6108
  if (el.type !== "image") continue;
@@ -5944,16 +6112,24 @@ function encodeImages(elements, imageCache, rasterScale) {
5944
6112
  }
5945
6113
  const img = imageCache.get(el.id);
5946
6114
  if (!img || typeof document === "undefined") continue;
6115
+ const width = Math.max(1, Math.ceil(el.size.w * rasterScale));
6116
+ const height = Math.max(1, Math.ceil(el.size.h * rasterScale));
6117
+ assertExportSize(width, height, resourceOptions);
5947
6118
  const canvas = document.createElement("canvas");
5948
- canvas.width = Math.max(1, Math.ceil(el.size.w * rasterScale));
5949
- canvas.height = Math.max(1, Math.ceil(el.size.h * rasterScale));
6119
+ canvas.width = width;
6120
+ canvas.height = height;
5950
6121
  const ctx = canvas.getContext("2d");
5951
- if (!ctx) continue;
6122
+ if (!ctx) {
6123
+ resourceOptions.onAssetError?.({ elementId: el.id, src: el.src, reason: "encode" });
6124
+ continue;
6125
+ }
5952
6126
  try {
5953
6127
  ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
5954
6128
  const uri = canvas.toDataURL();
5955
6129
  if (uri.startsWith("data:")) out.set(el.id, uri);
5956
- } catch {
6130
+ else resourceOptions.onAssetError?.({ elementId: el.id, src: el.src, reason: "encode" });
6131
+ } catch (cause) {
6132
+ resourceOptions.onAssetError?.({ elementId: el.id, src: el.src, reason: "encode", cause });
5957
6133
  }
5958
6134
  }
5959
6135
  return out;
@@ -10726,7 +10902,7 @@ var LaserTool = class {
10726
10902
  };
10727
10903
 
10728
10904
  // src/index.ts
10729
- var VERSION = "0.50.7";
10905
+ var VERSION = "0.51.0";
10730
10906
  // Annotate the CommonJS export names for ESM import in node:
10731
10907
  0 && (module.exports = {
10732
10908
  ArrowTool,