@pixerate/editor 0.109.0 → 0.111.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.
Files changed (66) hide show
  1. package/THIRD_PARTY_NOTICES.md +3 -0
  2. package/dist/blending-DXl06NSx.d.ts +486 -0
  3. package/dist/blending-aOE6Y3UE.d.cts +486 -0
  4. package/dist/chunk-7VOHOPLW.cjs +1136 -0
  5. package/dist/chunk-7VOHOPLW.cjs.map +1 -0
  6. package/dist/chunk-FTBZPI77.cjs +1 -0
  7. package/dist/chunk-FTBZPI77.cjs.map +1 -0
  8. package/dist/{chunk-RE77QYUF.js → chunk-HHZ6ZJTW.js} +880 -86
  9. package/dist/chunk-HHZ6ZJTW.js.map +1 -0
  10. package/dist/{chunk-WMBIJCHA.js → chunk-HNS7UHCM.js} +5 -1
  11. package/dist/{chunk-WMBIJCHA.js.map → chunk-HNS7UHCM.js.map} +1 -1
  12. package/dist/{chunk-CFMWALBM.js → chunk-LQEEDHOP.js} +2 -2
  13. package/dist/{chunk-D37BX2ZA.cjs → chunk-PHUMLSCW.cjs} +1141 -347
  14. package/dist/chunk-PHUMLSCW.cjs.map +1 -0
  15. package/dist/{chunk-AZTZRLDI.cjs → chunk-POGBUYWE.cjs} +92 -92
  16. package/dist/{chunk-AZTZRLDI.cjs.map → chunk-POGBUYWE.cjs.map} +1 -1
  17. package/dist/chunk-VKK5VS2T.js +1 -0
  18. package/dist/{chunk-SZ6KMZNM.cjs → chunk-XIOMMGPD.cjs} +6 -2
  19. package/dist/chunk-XIOMMGPD.cjs.map +1 -0
  20. package/dist/{chunk-3PHA3UKI.js → chunk-Z4BC6M74.js} +523 -121
  21. package/dist/chunk-Z4BC6M74.js.map +1 -0
  22. package/dist/{export-BLPLaE6Z.d.ts → export-DBNVwb6h.d.ts} +521 -12
  23. package/dist/{export-DSUc4fQD.d.cts → export-DoDhCszr.d.cts} +521 -12
  24. package/dist/image-editor/index.cjs +1630 -900
  25. package/dist/image-editor/index.cjs.map +1 -1
  26. package/dist/image-editor/index.d.cts +104 -16
  27. package/dist/image-editor/index.d.ts +104 -16
  28. package/dist/image-editor/index.js +776 -46
  29. package/dist/image-editor/index.js.map +1 -1
  30. package/dist/paint/index.cjs +28 -4
  31. package/dist/paint/index.cjs.map +1 -1
  32. package/dist/paint/index.d.cts +3 -89
  33. package/dist/paint/index.d.ts +3 -89
  34. package/dist/paint/index.js +27 -3
  35. package/dist/paint-editor/index.cjs +1052 -286
  36. package/dist/paint-editor/index.cjs.map +1 -1
  37. package/dist/paint-editor/index.d.cts +302 -23
  38. package/dist/paint-editor/index.d.ts +302 -23
  39. package/dist/paint-editor/index.js +848 -82
  40. package/dist/paint-editor/index.js.map +1 -1
  41. package/dist/raster/index.cjs +5 -3
  42. package/dist/raster/index.cjs.map +1 -1
  43. package/dist/raster/index.d.cts +2 -2
  44. package/dist/raster/index.d.ts +2 -2
  45. package/dist/raster/index.js +4 -2
  46. package/dist/{render-C-fO_mes.d.cts → render-BEriFuqK.d.cts} +1 -1
  47. package/dist/{render-OLIMk_v0.d.ts → render-tjzKaik-.d.ts} +1 -1
  48. package/dist/{surface-CkKOHGPm.d.cts → surface-C8RRg_Bh.d.cts} +109 -1
  49. package/dist/{surface-CkKOHGPm.d.ts → surface-C8RRg_Bh.d.ts} +109 -1
  50. package/package.json +1 -1
  51. package/skills/pixerate-image-editor/SKILL.md +27 -30
  52. package/skills/pixerate-image-editor/references/shapes-and-type.md +60 -0
  53. package/skills/pixerate-paint-editor/SKILL.md +49 -13
  54. package/dist/chunk-3PHA3UKI.js.map +0 -1
  55. package/dist/chunk-6BU5WU4R.cjs +0 -1
  56. package/dist/chunk-6BU5WU4R.cjs.map +0 -1
  57. package/dist/chunk-D37BX2ZA.cjs.map +0 -1
  58. package/dist/chunk-HUFQD4CB.cjs +0 -734
  59. package/dist/chunk-HUFQD4CB.cjs.map +0 -1
  60. package/dist/chunk-RE77QYUF.js.map +0 -1
  61. package/dist/chunk-SZ6KMZNM.cjs.map +0 -1
  62. package/dist/chunk-XYBJKUAU.js +0 -1
  63. package/dist/mixer-5ziHU8OS.d.cts +0 -282
  64. package/dist/mixer-DNqiYXYQ.d.ts +0 -282
  65. /package/dist/{chunk-CFMWALBM.js.map → chunk-LQEEDHOP.js.map} +0 -0
  66. /package/dist/{chunk-XYBJKUAU.js.map → chunk-VKK5VS2T.js.map} +0 -0
@@ -4,7 +4,7 @@ import {
4
4
  import {
5
5
  createCaptureTool
6
6
  } from "../chunk-REOJJIY3.js";
7
- import "../chunk-XYBJKUAU.js";
7
+ import "../chunk-VKK5VS2T.js";
8
8
  import {
9
9
  ADJUSTMENT_KEYS,
10
10
  ADJUSTMENT_KINDS,
@@ -17,8 +17,10 @@ import {
17
17
  D50,
18
18
  D65,
19
19
  DEFAULT_ADJUSTMENTS_VALUES,
20
+ DEFAULT_AUTO_LEADING,
20
21
  DEFAULT_DEPTH_MASK_VALUES,
21
22
  DEFAULT_MITER_LIMIT,
23
+ DEFAULT_TEXT_STYLE,
22
24
  DEFAULT_TRANSFORM_VALUES,
23
25
  DEVELOP_FILTER_ID,
24
26
  EMPTY_DISTANCE,
@@ -35,6 +37,7 @@ import {
35
37
  MAX_KELVIN,
36
38
  MIN_KELVIN,
37
39
  MIRED_PER_TEMPERATURE_UNIT,
40
+ PARAGRAPH_STYLE_FIELDS,
38
41
  PATTERN_BLOCK_KEYS,
39
42
  PSD_BLEND_KEYS,
40
43
  PsdError,
@@ -44,6 +47,9 @@ import {
44
47
  SHAPE_RENDER_MARGIN,
45
48
  SMART_RENDER_MARGIN,
46
49
  SUPPORTED_ADJUSTMENT_KEYS,
50
+ TEXT_ALIGNS,
51
+ TEXT_RENDER_MARGIN,
52
+ TEXT_STYLE_FIELDS,
47
53
  TINT_SCALE,
48
54
  TONE_LUT_SIZE,
49
55
  TRANSFORM_ANCHORS,
@@ -64,6 +70,7 @@ import {
64
70
  annotationLayerName,
65
71
  annotationOfLayer,
66
72
  annotationOfShape,
73
+ annotationOfType,
67
74
  applyAdjustmentLayers,
68
75
  applyAdjustments,
69
76
  applyBoxBlur,
@@ -84,6 +91,7 @@ import {
84
91
  buildToneLut,
85
92
  calculateCropDrag,
86
93
  canvasToImagePoint,
94
+ caretAt,
87
95
  clampCropToImage,
88
96
  colorBalanceChannel,
89
97
  combineSelections,
@@ -100,6 +108,7 @@ import {
100
108
  createAdjustmentLayer,
101
109
  createSelectionMask,
102
110
  createShapeLayer,
111
+ createTypeLayer,
103
112
  curveLutFromLevels,
104
113
  decodeLayerChannel,
105
114
  decodeLayerMask,
@@ -131,8 +140,11 @@ import {
131
140
  exportPsd,
132
141
  featherCoverage,
133
142
  filterMaskCoverage,
143
+ fitParagraphRuns,
144
+ fitRuns,
134
145
  flattenPath,
135
146
  flipCorners,
147
+ fontOf,
136
148
  fullSelection,
137
149
  geometryHomography,
138
150
  getAnnotationBounds,
@@ -183,6 +195,7 @@ import {
183
195
  layerTree,
184
196
  layerTypeLabel,
185
197
  layersFromLegacy,
198
+ layoutText,
186
199
  legacyAdjustmentLayer,
187
200
  legacyMaskOf,
188
201
  legacyView,
@@ -215,9 +228,11 @@ import {
215
228
  normalizeShapeStroke,
216
229
  normalizeSmartFilter,
217
230
  normalizeSubpaths,
231
+ normalizeText,
218
232
  offsetLayer,
219
233
  offsetShape,
220
234
  offsetSmartObject,
235
+ paragraphRanges,
221
236
  parseAdjustmentBlock,
222
237
  parseBlend,
223
238
  parseCssColor,
@@ -226,6 +241,8 @@ import {
226
241
  parsePatternBlock,
227
242
  parseSelectionMode,
228
243
  parseVersionedDescriptor,
244
+ patchParagraphStyle,
245
+ patchTextStyle,
229
246
  photoFilterMatrix,
230
247
  pixelLayerBounds,
231
248
  pixelLayers,
@@ -256,16 +273,21 @@ import {
256
273
  renderShape,
257
274
  renderShapeRect,
258
275
  renderSmartObject,
276
+ renderText,
259
277
  replaceContents,
278
+ replaceTextRange,
260
279
  requireLayer,
261
280
  requireOperation,
262
281
  requireShapeLayer,
263
282
  requireSmartObject,
283
+ requireTypeLayer,
264
284
  requireUnlocked,
265
285
  rerender,
266
286
  rerenderShape,
287
+ rerenderText,
267
288
  resizeSelection,
268
289
  resolveExternalAssets,
290
+ restyleRange,
269
291
  rgbToGray,
270
292
  rgbToHsl,
271
293
  runLayerFilters,
@@ -301,16 +323,28 @@ import {
301
323
  smartContentsFile,
302
324
  smartObjectLayers,
303
325
  smoothCoverage,
326
+ spliceRuns,
304
327
  srgbToLinear,
328
+ styleAt,
305
329
  tempTintToXy,
306
330
  temperatureToKelvin,
331
+ textIndexAt,
332
+ textMatrix,
333
+ textOutlines,
334
+ textRange,
335
+ toTextSpace,
307
336
  toneCurve,
308
337
  toneLuts,
309
338
  transformHomography,
310
339
  transformMap,
311
340
  transformShapeComponents,
341
+ transformTypeLayer,
312
342
  transformedBox,
313
343
  transformedPoint,
344
+ typeAsPixelLayer,
345
+ typeFrame,
346
+ typeFromAnnotation,
347
+ typeLayerName,
314
348
  ungroupLayer,
315
349
  unionRect,
316
350
  unprojectCanvasPoint,
@@ -319,6 +353,7 @@ import {
319
353
  updateLayerMaskProps,
320
354
  updateShapeLayer,
321
355
  updateSmartFilter,
356
+ updateTypeLayer,
322
357
  uvToXy,
323
358
  versionedDescriptorBytes,
324
359
  vibranceLinear,
@@ -334,7 +369,7 @@ import {
334
369
  writePsd,
335
370
  xyToTempTint,
336
371
  xyToUv
337
- } from "../chunk-RE77QYUF.js";
372
+ } from "../chunk-HHZ6ZJTW.js";
338
373
  import {
339
374
  DEFAULT_GLOBAL_LIGHT,
340
375
  DIRECT_GAUSSIAN_MAX_SIGMA,
@@ -398,7 +433,7 @@ import {
398
433
  validateLayerEffect,
399
434
  validateLayerEffects,
400
435
  warpSurface
401
- } from "../chunk-CFMWALBM.js";
436
+ } from "../chunk-LQEEDHOP.js";
402
437
  import {
403
438
  applyHomography,
404
439
  boxQuad,
@@ -410,8 +445,10 @@ import {
410
445
  } from "../chunk-GX332W2C.js";
411
446
  import {
412
447
  CoverageMap,
448
+ DEFAULT_BLENDER,
413
449
  DEFAULT_BRUSH,
414
450
  DEFAULT_MIXER,
451
+ DEFAULT_SMUDGE,
415
452
  DabWalker,
416
453
  EMPTY_MIXER_LOAD,
417
454
  MAX_BRUSH_SIZE,
@@ -419,23 +456,33 @@ import {
419
456
  MIXER_PICKUP_RATE,
420
457
  MIXING_MODELS,
421
458
  MixerStroke,
459
+ PaintExchange,
460
+ SmudgeTip,
422
461
  StrokeCoverage,
462
+ TileMedium,
423
463
  applyStrokeToTile,
464
+ blendRadius,
465
+ blenderDab,
424
466
  cleanMixer,
425
467
  coverageTileCoord,
468
+ createBlenderScratch,
426
469
  createPigmentTools,
427
470
  dabCoverage,
428
471
  dabRect,
472
+ exchangeRates,
429
473
  loadMixer,
474
+ mixerRates,
475
+ normalizeBlender,
430
476
  normalizeBrush,
431
477
  normalizeMixer,
478
+ normalizeSmudge,
432
479
  rasterizeDab,
433
480
  smoothPoints,
434
481
  tipFalloff,
435
482
  tipReach,
436
483
  tipShape,
437
484
  walkDabs
438
- } from "../chunk-3PHA3UKI.js";
485
+ } from "../chunk-Z4BC6M74.js";
439
486
  import {
440
487
  BLEND_MODES,
441
488
  PAINTERLY_BLEND_MODES,
@@ -470,6 +517,7 @@ import {
470
517
  mixPigments,
471
518
  normalizeBlendMode,
472
519
  packRgba,
520
+ pigment8,
473
521
  readPixel,
474
522
  readRegion,
475
523
  reflectanceToRgb,
@@ -488,17 +536,20 @@ import {
488
536
  translateSurface,
489
537
  uniformTile,
490
538
  writeRegion
491
- } from "../chunk-WMBIJCHA.js";
539
+ } from "../chunk-HNS7UHCM.js";
492
540
  import {
493
541
  WARP_STYLES,
494
542
  WARP_STYLE_LABELS,
495
543
  applyMatrix,
544
+ bezierToSubpaths,
496
545
  dragWarpMeshPoint,
497
546
  evalWarpMesh,
498
547
  identityWarpMesh,
548
+ invertMatrix,
499
549
  multiplyMatrices,
500
550
  pullWarpMesh,
501
551
  resizeWarpMesh,
552
+ rotateMatrix,
502
553
  roundSubpaths,
503
554
  translateMatrix,
504
555
  warpMeshColumns,
@@ -687,7 +738,7 @@ function needsLegacyBase(input) {
687
738
  if (hasResampledLegacyMask(stack.layers, canvas.width, canvas.height)) return true;
688
739
  return false;
689
740
  }
690
- var isPlainAnnotation = (l) => l.type === "annotation" ? l.opacity === 1 && l.blend === "normal" && !l.clipped && !l.mask : l.type === "shape" && Boolean(l.annotation) && l.opacity === 1 && l.blend === (l.annotation.blend ?? "normal") && !l.clipped && !l.mask && !l.effects && (l.fillOpacity ?? 1) === 1;
741
+ var isPlainAnnotation = (l) => l.type === "annotation" ? l.opacity === 1 && l.blend === "normal" && !l.clipped && !l.mask : (l.type === "shape" || l.type === "type") && Boolean(l.annotation) && l.opacity === 1 && l.blend === (l.annotation.blend ?? "normal") && !l.clipped && !l.mask && !l.effects && (l.fillOpacity ?? 1) === 1;
691
742
  var overlayViews = /* @__PURE__ */ new WeakMap();
692
743
  function overlayView(l) {
693
744
  if (l.type === "annotation") return l;
@@ -703,7 +754,7 @@ function splitOverlayAnnotations(layers) {
703
754
  let i = layers.length;
704
755
  while (i > 0) {
705
756
  const l = layers[i - 1];
706
- if ((l.type === "annotation" || l.type === "shape" && l.annotation) && (!l.visible || isPlainAnnotation(l))) i--;
757
+ if ((l.type === "annotation" || (l.type === "shape" || l.type === "type") && l.annotation) && (!l.visible || isPlainAnnotation(l))) i--;
707
758
  else break;
708
759
  }
709
760
  return {
@@ -2428,6 +2479,159 @@ function refineSelection(pixels, mask, params = {}) {
2428
2479
  return createSelectionMask(mask.width, mask.height, out);
2429
2480
  }
2430
2481
 
2482
+ // src/image-editor/layers/text-shapers.ts
2483
+ var GENERIC = /* @__PURE__ */ new Set(["serif", "sans-serif", "monospace", "cursive", "fantasy", "system-ui", "ui-serif", "ui-sans-serif", "ui-monospace", "ui-rounded", "math", "emoji", "fangsong"]);
2484
+ function cssFontFamily(family) {
2485
+ const f = family.trim();
2486
+ if (GENERIC.has(f.toLowerCase()) || f.includes(",") || /^["']/.test(f)) return f;
2487
+ return /^[A-Za-z_-][\w -]*$/.test(f) && !/\s{2}/.test(f) ? f : `"${f.replace(/"/g, '\\"')}"`;
2488
+ }
2489
+ function cssFont(font, family = font.family) {
2490
+ return `${font.italic ? "italic " : ""}${font.weight} ${font.size}px ${cssFontFamily(family)}`;
2491
+ }
2492
+ function createCanvasTextShaper(options = {}) {
2493
+ const make = options.createCanvas ?? ((w, h) => {
2494
+ if (typeof OffscreenCanvas !== "undefined") return new OffscreenCanvas(w, h);
2495
+ if (typeof document !== "undefined") {
2496
+ const c = document.createElement("canvas");
2497
+ c.width = w;
2498
+ c.height = h;
2499
+ return c;
2500
+ }
2501
+ return null;
2502
+ });
2503
+ const canvas = make(1, 1);
2504
+ const ctx = canvas?.getContext("2d");
2505
+ if (!ctx || typeof ctx.measureText !== "function") return null;
2506
+ const fallback = options.fallbackFamily ?? "sans-serif";
2507
+ const available = /* @__PURE__ */ new Map();
2508
+ const widths = /* @__PURE__ */ new Map();
2509
+ const metrics = /* @__PURE__ */ new Map();
2510
+ const probe = "mmmmmmmmmmlli1WQ@#";
2511
+ const width = (font, text) => {
2512
+ ctx.font = font;
2513
+ return ctx.measureText(text).width;
2514
+ };
2515
+ const isAvailable = (family) => {
2516
+ const f = family.trim();
2517
+ if (GENERIC.has(f.toLowerCase()) || f.includes(",")) return true;
2518
+ let hit = available.get(f);
2519
+ if (hit === void 0) {
2520
+ const q = cssFontFamily(f);
2521
+ hit = width(`72px ${q}, monospace`, probe) !== width("72px monospace", probe) || width(`72px ${q}, serif`, probe) !== width("72px serif", probe);
2522
+ available.set(f, hit);
2523
+ }
2524
+ return hit;
2525
+ };
2526
+ return {
2527
+ id: "canvas",
2528
+ resolve(font) {
2529
+ const ok = isAvailable(font.family);
2530
+ return { family: ok ? font.family : fallback, available: ok };
2531
+ },
2532
+ metrics(font) {
2533
+ const key = cssFont(font);
2534
+ let m = metrics.get(key);
2535
+ if (!m) {
2536
+ ctx.font = key;
2537
+ ctx.textBaseline = "alphabetic";
2538
+ const t = ctx.measureText("Hg");
2539
+ const ascent = t.emHeightAscent ?? t.fontBoundingBoxAscent ?? font.size * 0.8;
2540
+ const descent = t.emHeightDescent ?? t.fontBoundingBoxDescent ?? font.size * 0.2;
2541
+ m = { ascent: Number.isFinite(ascent) ? ascent : font.size * 0.8, descent: Number.isFinite(descent) ? descent : font.size * 0.2 };
2542
+ metrics.set(key, m);
2543
+ }
2544
+ return m;
2545
+ },
2546
+ measure(text, font) {
2547
+ const key = `${cssFont(font)}|${text}`;
2548
+ let w = widths.get(key);
2549
+ if (!w) {
2550
+ ctx.font = cssFont(font);
2551
+ w = [0];
2552
+ for (let i = 1; i <= text.length; i++) w.push(ctx.measureText(text.slice(0, i)).width);
2553
+ if (widths.size > 2e3) widths.clear();
2554
+ widths.set(key, w);
2555
+ }
2556
+ return w;
2557
+ },
2558
+ draw(target, text, font, x, y) {
2559
+ target.font = cssFont(font);
2560
+ target.textBaseline = "alphabetic";
2561
+ target.textAlign = "left";
2562
+ target.fillText(text, x, y);
2563
+ },
2564
+ families() {
2565
+ const out = /* @__PURE__ */ new Set(["sans-serif", "serif", "monospace"]);
2566
+ const fonts = typeof document !== "undefined" ? document.fonts : void 0;
2567
+ if (fonts && typeof fonts.forEach === "function") fonts.forEach((f) => out.add(f.family.replace(/^["']|["']$/g, "")));
2568
+ return [...out];
2569
+ }
2570
+ };
2571
+ }
2572
+ function createOutlineTextShaper(fonts, options = {}) {
2573
+ if (!fonts.length) throw new Error("createOutlineTextShaper needs at least one font.");
2574
+ const entries = fonts.map((f) => {
2575
+ const e = "face" in f ? f : { face: f };
2576
+ const names = e.face.names;
2577
+ const family = e.family ?? names?.fontFamily?.en ?? Object.values(names?.fontFamily ?? {})[0] ?? "Font";
2578
+ const weight = e.weight ?? e.face.tables?.os2?.usWeightClass ?? 400;
2579
+ const italic = e.italic ?? Boolean((e.face.tables?.os2?.fsSelection ?? 0) & 1);
2580
+ const postScript = names?.postScriptName?.en ?? Object.values(names?.postScriptName ?? {})[0];
2581
+ return { face: e.face, family, key: family.toLowerCase(), weight, italic, postScript };
2582
+ });
2583
+ const fallbackKey = (options.fallback ?? entries[0].family).toLowerCase();
2584
+ const pick2 = (font) => {
2585
+ if (font.postScriptName) {
2586
+ const exact = entries.find((e) => e.postScript === font.postScriptName);
2587
+ if (exact) return { entry: exact, available: true };
2588
+ }
2589
+ const key = font.family.trim().toLowerCase();
2590
+ let pool = entries.filter((e) => e.key === key);
2591
+ const available = pool.length > 0;
2592
+ if (!available) pool = entries.filter((e) => e.key === fallbackKey);
2593
+ if (!pool.length) pool = entries;
2594
+ const styled = pool.filter((e) => e.italic === font.italic);
2595
+ const list = styled.length ? styled : pool;
2596
+ const entry = list.reduce((best, e) => Math.abs(e.weight - font.weight) < Math.abs(best.weight - font.weight) ? e : best);
2597
+ return { entry, available };
2598
+ };
2599
+ const toPath = (cmds) => {
2600
+ const out = [];
2601
+ for (const c of cmds) {
2602
+ if (c.type === "M" || c.type === "L") out.push({ type: c.type, x: c.x, y: c.y });
2603
+ else if (c.type === "C") out.push({ type: "C", x1: c.x1, y1: c.y1, x2: c.x2, y2: c.y2, x: c.x, y: c.y });
2604
+ else if (c.type === "Q") out.push({ type: "Q", x1: c.x1, y1: c.y1, x: c.x, y: c.y });
2605
+ else out.push({ type: "Z" });
2606
+ }
2607
+ return out;
2608
+ };
2609
+ return {
2610
+ id: options.id ?? `outline:${entries.map((e) => `${e.family}/${e.weight}${e.italic ? "i" : ""}`).join(",")}`,
2611
+ resolve(font) {
2612
+ const { entry, available } = pick2(font);
2613
+ return { family: entry.family, available };
2614
+ },
2615
+ metrics(font) {
2616
+ const { face } = pick2(font).entry;
2617
+ const k = font.size / face.unitsPerEm;
2618
+ return { ascent: face.ascender * k, descent: -face.descender * k };
2619
+ },
2620
+ measure(text, font) {
2621
+ const { face } = pick2(font).entry;
2622
+ const out = [0];
2623
+ for (let i = 1; i <= text.length; i++) out.push(face.getAdvanceWidth(text.slice(0, i), font.size, { kerning: true }));
2624
+ return out;
2625
+ },
2626
+ outline(text, font, x, y) {
2627
+ return toPath(pick2(font).entry.face.getPath(text, x, y, font.size, { kerning: true }).commands);
2628
+ },
2629
+ families() {
2630
+ return [...new Set(entries.map((e) => e.family))];
2631
+ }
2632
+ };
2633
+ }
2634
+
2431
2635
  // src/image-editor/free-transform.ts
2432
2636
  var FREE_TRANSFORM_CONTENTS = ["auto", "layer", "selection", "selection-copy", "selection-outline"];
2433
2637
  var fail = (message) => {
@@ -2499,7 +2703,7 @@ var FreeTransformSession = class {
2499
2703
  }
2500
2704
  const targetId = init.layerId ?? host.activeLayerId();
2501
2705
  const target = targetId && content !== "selection-outline" ? findLayer(host.layers(), targetId)?.layer : void 0;
2502
- const smartTarget = (target?.type === "smart" || target?.type === "shape") && (init.channel ?? (target.id === host.activeLayerId() ? host.activeTarget() : "pixels")) !== "mask";
2706
+ const smartTarget = (target?.type === "smart" || target?.type === "shape" || target?.type === "type") && (init.channel ?? (target.id === host.activeLayerId() ? host.activeTarget() : "pixels")) !== "mask";
2503
2707
  if (content === "auto") content = selected && !smartTarget ? "selection" : "layer";
2504
2708
  this.content = content;
2505
2709
  if (content !== "layer" && !selected) fail(`Nothing is selected; ${content} needs a selection. Select first, or use content layer.`);
@@ -2526,7 +2730,7 @@ var FreeTransformSession = class {
2526
2730
  const linked = mask?.linked !== false;
2527
2731
  const maskAlone = channel === "mask" && (!linked || layer.type !== "pixel");
2528
2732
  if (channel === "mask" && !mask) fail(layer.mask?.source ? `Layer "${layer.name || id}" has a depth mask, which is generated, not transformed.` : `Layer "${layer.name || id}" has no mask to transform; add one with add_layer_mask, or transform its pixels.`);
2529
- if (!maskAlone && layer.type !== "pixel" && layer.type !== "smart" && layer.type !== "shape") fail(`Layer "${layer.name || id}" is a ${layer.type} layer; Free Transform works on pixel, shape and smart object layers, layer masks (channel mask) and selections.`);
2733
+ if (!maskAlone && layer.type !== "pixel" && layer.type !== "smart" && layer.type !== "shape" && layer.type !== "type") fail(`Layer "${layer.name || id}" is a ${layer.type} layer; Free Transform works on pixel, shape, type and smart object layers, layer masks (channel mask) and selections.`);
2530
2734
  if (!layer.visible && content !== "selection-copy") fail(`Layer "${layer.name || id}" is hidden; show it before you transform it.`);
2531
2735
  if (content !== "selection-copy") {
2532
2736
  requireUnlocked(layer, "position", "transform it");
@@ -2548,6 +2752,26 @@ var FreeTransformSession = class {
2548
2752
  this.shown = this.startLayers;
2549
2753
  return;
2550
2754
  }
2755
+ if (layer.type === "type" && !maskAlone) {
2756
+ if (content !== "layer") fail(`Layer "${layer.name || id}" is a type layer, which transforms as a whole; use content layer (or rasterize it to transform part of it).`);
2757
+ const frame = typeFrame(layer, host.textEnv().shaper);
2758
+ const m = textMatrix(layer);
2759
+ const at = (x, y) => ({ x: m[0] * x + m[2] * y + m[4], y: m[1] * x + m[3] * y + m[5] });
2760
+ this.typeTarget = layer;
2761
+ this.typeLayer = true;
2762
+ this.box = frame;
2763
+ this.startGeometry = {
2764
+ box: frame,
2765
+ corners: [at(frame.x, frame.y), at(frame.x + frame.width, frame.y), at(frame.x + frame.width, frame.y + frame.height), at(frame.x, frame.y + frame.height)],
2766
+ warp: null
2767
+ };
2768
+ this.geometryValue = this.startGeometry;
2769
+ this.startInterp = this.interp;
2770
+ this.moves = mask && linked ? ["pixels", "mask"] : ["pixels"];
2771
+ if (mask && linked) this.parts.push({ what: "mask", offset: o, moving: mask.surface, base: null, reduced: /* @__PURE__ */ new Map() });
2772
+ this.shown = this.startLayers;
2773
+ return;
2774
+ }
2551
2775
  if (layer.type === "shape" && !maskAlone) {
2552
2776
  if (content !== "layer") fail(`Layer "${layer.name || id}" is a shape layer, which transforms as a whole; use content layer (or rasterize it to transform part of it).`);
2553
2777
  const g = shapeGeometryBounds(layer);
@@ -2650,6 +2874,9 @@ var FreeTransformSession = class {
2650
2874
  /** True when the layer is a shape layer (the transform maps its paths and renders them again). */
2651
2875
  shapeLayer = false;
2652
2876
  shape = null;
2877
+ /** True when the layer is a type layer (the transform sets its text matrix; affine only). */
2878
+ typeLayer = false;
2879
+ typeTarget = null;
2653
2880
  geometryValue;
2654
2881
  smart = null;
2655
2882
  smartReduced = /* @__PURE__ */ new Map();
@@ -2698,6 +2925,9 @@ var FreeTransformSession = class {
2698
2925
  };
2699
2926
  if (next.corners.length !== 4 || !next.corners.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y))) fail("corners must be four finite points: top-left, top-right, bottom-right, bottom-left.");
2700
2927
  if (!transformMap(next)) fail("These corners fold the frame over itself or through the horizon; keep the quad convex.");
2928
+ if (this.typeTarget && (hasWarp(next) || !matrixFromCorners(next.box, next.corners))) {
2929
+ fail("Type layers take scale, rotate, skew and flips only (the corners must stay a parallelogram, no warp); rasterize the layer (rasterize_layer) for distort, perspective or warp.");
2930
+ }
2701
2931
  this.geometryValue = next;
2702
2932
  if (preview !== false) this.render(preview);
2703
2933
  return next;
@@ -2705,6 +2935,7 @@ var FreeTransformSession = class {
2705
2935
  /** Resamples every part for the current geometry and shows it (or returns it, for commit). */
2706
2936
  resample(final, preview = {}) {
2707
2937
  if (this.smart) return { layers: this.resampleSmart(final, preview), selection: this.startSelection };
2938
+ if (this.typeTarget) return { layers: this.resampleType(), selection: this.startSelection };
2708
2939
  const g = this.geometryValue;
2709
2940
  const identity = isIdentityTransform(g);
2710
2941
  const warp = hasWarp(g);
@@ -2785,6 +3016,27 @@ var FreeTransformSession = class {
2785
3016
  const surface = renderShape(moved, this.host.shapeEnv(), final ? {} : { clip: preview.clip });
2786
3017
  return mapLayer(this.startLayers, layer.id, () => ({ ...moved, surface }));
2787
3018
  }
3019
+ /**
3020
+ * A type layer with its text matrix set from the corners (affine only: scale, rotate, skew and
3021
+ * flips), drawn again; a linked mask follows by the change's homography.
3022
+ */
3023
+ resampleType() {
3024
+ const layer = this.typeTarget;
3025
+ const g = this.geometryValue;
3026
+ if (hasWarp(g)) fail("Type layers cannot be warped here; rasterize the layer first (rasterize_layer), or transform it with scale, rotate and skew.");
3027
+ const m = matrixFromCorners(g.box, g.corners);
3028
+ if (!m) fail("Type layers take scale, rotate, skew and flips only (the corners must stay a parallelogram); rasterize the layer for distort or perspective.");
3029
+ const next = { ...layer, transform: m };
3030
+ delete next.annotation;
3031
+ let layers = mapLayer(this.startLayers, layer.id, () => rerenderText(next, this.host.textEnv()));
3032
+ const change = multiplyHomographies(homographyFromAffine(invertMatrix(textMatrix(layer))), homographyFromAffine(m));
3033
+ for (const part of this.parts) {
3034
+ if (part.what !== "mask") continue;
3035
+ const surface = transformSurface(part.moving, change, { interpolation: this.interp === "nearest" ? "nearest" : "bilinear" });
3036
+ layers = mapLayer(layers, layer.id, (l) => ({ ...l, mask: { ...l.mask, surface } }));
3037
+ }
3038
+ return layers;
3039
+ }
2788
3040
  /** A smart object re-rendered from its contents for the current geometry (previews leave the smart filters out). */
2789
3041
  resampleSmart(final, preview) {
2790
3042
  const layer = this.smart;
@@ -2808,7 +3060,7 @@ var FreeTransformSession = class {
2808
3060
  /** Whether the geometry (or the interpolation of a smart object) differs from where it started. */
2809
3061
  isChanged() {
2810
3062
  const g = this.geometryValue;
2811
- if (!this.smart) return !isIdentityTransform(g);
3063
+ if (!this.smart && !this.typeTarget) return !isIdentityTransform(g);
2812
3064
  const a = this.startGeometry;
2813
3065
  const same = g.corners.every((c, i) => Math.abs(c.x - a.corners[i].x) < 1e-9 && Math.abs(c.y - a.corners[i].y) < 1e-9);
2814
3066
  const warp = (w, box) => w && hasWarp({ box, corners: a.corners, warp: w }) ? JSON.stringify(w) : "";
@@ -2845,7 +3097,7 @@ var FreeTransformSession = class {
2845
3097
  const id = this.copyId ?? this.layerId;
2846
3098
  const layer = id ? findLayer(layers, id)?.layer : void 0;
2847
3099
  if (this.moves.includes("pixels") && layer?.type === "pixel") bounds = toDoc(contentBounds(this.host.pixelsOf(layer)), layer.offset);
2848
- else if (this.moves.includes("pixels") && (layer?.type === "smart" || layer?.type === "shape")) bounds = contentBounds(layer.surface);
3100
+ else if (this.moves.includes("pixels") && (layer?.type === "smart" || layer?.type === "shape" || layer?.type === "type")) bounds = contentBounds(layer.surface);
2849
3101
  else if (this.moves.includes("mask") && layer?.mask) bounds = toDoc(contentBounds(layer.mask.surface), layer.type === "pixel" ? layer.offset : { x: 0, y: 0 });
2850
3102
  else if (this.moves.includes("selection")) bounds = selection?.bounds ? { ...selection.bounds } : null;
2851
3103
  return {
@@ -3099,6 +3351,7 @@ var ImageEditorController = class {
3099
3351
  compositor = new LayerCompositor();
3100
3352
  constructor(options = {}) {
3101
3353
  this.maxHistoryDepth = options.maxHistoryDepth ?? 50;
3354
+ if (options.textShaper !== void 0) this.textShaperValue = options.textShaper;
3102
3355
  this.maxHistoryBytes = options.maxHistoryBytes ?? 512 * 1024 * 1024;
3103
3356
  this.onStateChange = options.onStateChange;
3104
3357
  this.renderer = new ImageEditorRenderer();
@@ -3805,6 +4058,21 @@ var ImageEditorController = class {
3805
4058
  const b = shapeGeometryBounds(moved) ?? { x: 0, y: 0 };
3806
4059
  return { offset: { x: Math.round(b.x), y: Math.round(b.y) }, bounds: this.getLayerBounds(id) };
3807
4060
  }
4061
+ if (layer.type === "type") {
4062
+ const m = layer.transform ?? [1, 0, 0, 1, 0, 0];
4063
+ const dx = move.x !== void 0 ? move.x - m[4] : move.dx ?? 0;
4064
+ const dy = move.y !== void 0 ? move.y - m[5] : move.dy ?? 0;
4065
+ if (!Number.isFinite(dx) || !Number.isFinite(dy)) throw new LayerOperationError("x and y must be numbers.");
4066
+ requireUnlocked(layer, "position", "move it");
4067
+ const mx = Math.round(dx);
4068
+ const my = Math.round(dy);
4069
+ let moved = { ...transformTypeLayer(layer, [1, 0, 0, 1, mx, my]), surface: translateSurface(layer.surface, mx, my) };
4070
+ const mask = layer.mask;
4071
+ if (mask && mask.linked !== false && !mask.source && (mx || my)) moved = { ...moved, mask: { ...mask, surface: translateSurface(mask.surface, mx, my) } };
4072
+ this.commitLayers(mapLayer(this.state.layers, id, () => moved), void 0, commitToHistory);
4073
+ const t = moved.transform ?? [1, 0, 0, 1, 0, 0];
4074
+ return { offset: { x: Math.round(t[4]), y: Math.round(t[5]) }, bounds: this.getLayerBounds(id) };
4075
+ }
3808
4076
  const current = layer.type === "pixel" ? layer.offset : { x: 0, y: 0 };
3809
4077
  const x = move.x ?? current.x + (move.dx ?? 0);
3810
4078
  const y = move.y ?? current.y + (move.dy ?? 0);
@@ -3953,6 +4221,7 @@ var ImageEditorController = class {
3953
4221
  canvas: () => this.naturalSize,
3954
4222
  pixelsOf: (layer) => this.surfaceOf(layer),
3955
4223
  shapeEnv: () => this.shapeEnv(),
4224
+ textEnv: () => this.textEnv(),
3956
4225
  capture: () => this.captureHistoryEntry(),
3957
4226
  show: (layers, selection, active) => {
3958
4227
  this.state.layers = layers;
@@ -4053,7 +4322,7 @@ var ImageEditorController = class {
4053
4322
  if (turn !== 90 && turn !== -90 && turn !== 180) throw new LayerOperationError(`turn must be 90, -90 or 180, got ${String(turn)}.`);
4054
4323
  const session = this.beginFreeTransform({ interpolation: "nearest", ...init });
4055
4324
  try {
4056
- const b = session.smartObject ? transformedBox(session.startGeometry) ?? session.box : session.box;
4325
+ const b = session.smartObject || session.typeLayer ? transformedBox(session.startGeometry) ?? session.box : session.box;
4057
4326
  const cx = b.x + b.width / 2;
4058
4327
  const cy = b.y + b.height / 2;
4059
4328
  const ox = turn === 180 ? 0 : Math.round(cx - b.height / 2) - (cx - b.height / 2);
@@ -4773,6 +5042,7 @@ var ImageEditorController = class {
4773
5042
  } else {
4774
5043
  if (layer.type === "smart") throw new LayerOperationError(`Layer "${layer.name || id}" is a smart object; its pixels come from its contents. Rasterize it (rasterize_smart_object), edit its contents (edit_smart_object_contents), or ${action} on its mask or filter mask.`);
4775
5044
  if (layer.type === "shape") throw new LayerOperationError(`Layer "${layer.name || id}" is a shape layer; its pixels are drawn from its paths. Rasterize it first (rasterize_layer), or ${action} on its mask (channel mask).`);
5045
+ if (layer.type === "type") throw new LayerOperationError(`Layer "${layer.name || id}" is a type layer; its pixels are drawn from its text. Rasterize it first (rasterize_layer), or ${action} on its mask (channel mask).`);
4776
5046
  if (layer.type !== "pixel") throw new LayerOperationError(`Layer "${layer.name || id}" is a ${layer.type} layer; ${action} on a pixel layer, or on this layer's mask (channel mask).`);
4777
5047
  requireUnlocked(layer, "pixels", action);
4778
5048
  }
@@ -5478,7 +5748,11 @@ var ImageEditorController = class {
5478
5748
  // --- Annotations & Layers ---
5479
5749
  /** The layer showing annotation `id`: an annotation layer, or a shape layer migrated from it. */
5480
5750
  annotationLayer(id) {
5481
- return flattenLayers(this.state.layers).find((l) => (l.type === "annotation" || l.type === "shape") && annotationOfLayer(l)?.id === id);
5751
+ return flattenLayers(this.state.layers).find((l) => (l.type === "annotation" || l.type === "shape" || l.type === "type") && annotationOfLayer(l)?.id === id);
5752
+ }
5753
+ /** What migrating an annotation needs: the canvas size, patterns and the text shaper. */
5754
+ annotationEnv() {
5755
+ return { ...this.shapeEnv(), ...this.textEnv() };
5482
5756
  }
5483
5757
  /**
5484
5758
  * Adds an annotation as its own layer (top of the stack unless `aboveId` is given). Pen,
@@ -5490,7 +5764,7 @@ var ImageEditorController = class {
5490
5764
  this.recordHistory();
5491
5765
  const id = `ann_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
5492
5766
  const fullAnnotation = { ...annotation, id };
5493
- const layer = annotationLayerFor(fullAnnotation, this.shapeEnv());
5767
+ const layer = annotationLayerFor(fullAnnotation, this.annotationEnv());
5494
5768
  const loc = placement.aboveId ? findLayer(this.state.layers, placement.aboveId) : null;
5495
5769
  this.state.layers = loc ? insertLayer(this.state.layers, layer, loc.parent?.id ?? null, loc.index + 1) : insertLayer(this.state.layers, layer, null);
5496
5770
  this.state.selectedAnnotationId = id;
@@ -5510,24 +5784,14 @@ var ImageEditorController = class {
5510
5784
  }
5511
5785
  this.notify();
5512
5786
  }
5513
- /** `layer` (an annotation or migrated shape layer) showing `annotation` instead, keeping its other properties. */
5787
+ /** `layer` (an annotation, or a shape or type layer migrated from one) showing `annotation` instead, keeping its other properties. */
5514
5788
  layerForAnnotation(layer, annotation, name) {
5515
- const shape = shapeFromAnnotation(annotation);
5516
- const { annotation: _old, ...props } = layer;
5517
- const blend = annotation.blend && annotation.blend !== "normal" ? annotation.blend : "normal";
5518
- if (shape) {
5519
- const base = { ...props, name, blend: layer.type === "annotation" && layer.blend !== "normal" ? layer.blend : blend };
5520
- if (layer.type === "shape") {
5521
- const fresh = rerenderShape({ ...base, components: shape.components, fill: shape.fill, stroke: shape.stroke }, this.shapeEnv());
5522
- delete fresh.fillRule;
5523
- return { ...fresh, annotation };
5524
- }
5525
- const created = createShapeLayer({ ...shape, name, blend: base.blend }, this.shapeEnv(), layer.id);
5526
- const { type: _t2, ...rest2 } = base;
5527
- return { ...rest2, ...created, annotation };
5528
- }
5529
- const { type: _t, components: _c, fill: _f, stroke: _s, surface: _sf, offset: _o, fillRule: _r, fillOpacity: _fo, psd: _p, ...rest } = props;
5530
- return { ...rest, type: "annotation", name, blend: layer.type === "shape" ? "normal" : layer.blend, annotation };
5789
+ const fresh = annotationLayerFor(annotation, this.annotationEnv());
5790
+ const keep = { id: layer.id, visible: layer.visible, opacity: layer.opacity };
5791
+ for (const k of ["clipped", "mask", "locks", "effects"]) if (layer[k] !== void 0) keep[k] = layer[k];
5792
+ if (fresh.type !== "annotation" && layer.type !== "annotation" && layer.fillOpacity !== void 0) keep.fillOpacity = layer.fillOpacity;
5793
+ const blend = fresh.type === "annotation" ? layer.type === "annotation" ? layer.blend : "normal" : layer.type === "annotation" && layer.blend !== "normal" ? layer.blend : fresh.blend;
5794
+ return { ...fresh, ...keep, name, blend };
5531
5795
  }
5532
5796
  removeAnnotation(id) {
5533
5797
  this.recordHistory();
@@ -5542,7 +5806,7 @@ var ImageEditorController = class {
5542
5806
  clearAnnotations() {
5543
5807
  this.recordHistory();
5544
5808
  let layers = this.state.layers;
5545
- for (const l of flattenLayers(layers)) if (l.type === "annotation" || l.type === "shape" && l.annotation) layers = mapLayer(layers, l.id, () => null);
5809
+ for (const l of flattenLayers(layers)) if (l.type === "annotation" || (l.type === "shape" || l.type === "type") && l.annotation) layers = mapLayer(layers, l.id, () => null);
5546
5810
  this.state.layers = layers;
5547
5811
  this.state.selectedAnnotationId = null;
5548
5812
  this.notify();
@@ -5781,6 +6045,233 @@ var ImageEditorController = class {
5781
6045
  this.commitLayers(setLayerMask(this.state.layers, target.id, { surface, defaultValue: 0 }));
5782
6046
  return target.id;
5783
6047
  }
6048
+ // --- Type layers ---
6049
+ textShaperValue;
6050
+ /** The shaper that lays out and draws type layers (`null` when there is none here). */
6051
+ getTextShaper() {
6052
+ if (this.textShaperValue === void 0) this.textShaperValue = createCanvasTextShaper({ createCanvas: (w, h) => this.renderer.createCanvas(w, h) });
6053
+ return this.textShaperValue;
6054
+ }
6055
+ /** Sets the text shaper (fonts) type layers lay out and draw with; existing layers redraw on their next edit. */
6056
+ setTextShaper(shaper) {
6057
+ this.textShaperValue = shaper;
6058
+ this.notify();
6059
+ }
6060
+ /** What type layers lay out and draw with. */
6061
+ textEnv() {
6062
+ return { ...this.naturalSize, shaper: this.getTextShaper(), createCanvas: (w, h) => this.renderer.createCanvas(w, h) };
6063
+ }
6064
+ /** Font families the shaper offers (generic families at least). */
6065
+ listFonts() {
6066
+ return this.getTextShaper()?.families?.() ?? ["sans-serif", "serif", "monospace"];
6067
+ }
6068
+ typeInfo(id) {
6069
+ const layer = requireTypeLayer(this.state.layers, id);
6070
+ const ranges = (runs) => {
6071
+ let at = 0;
6072
+ return runs.map((r) => {
6073
+ const out = { start: at, end: at + r.length, style: JSON.parse(JSON.stringify(r.style)) };
6074
+ at += r.length;
6075
+ return out;
6076
+ });
6077
+ };
6078
+ const shaper = this.getTextShaper();
6079
+ let lines = [];
6080
+ let missing = layer.missingFonts ?? [];
6081
+ if (shaper) {
6082
+ const layout = layoutText(layer, shaper);
6083
+ const r = (n) => Math.round(n * 1e3) / 1e3;
6084
+ lines = layout.lines.map((l) => ({ start: l.start, end: l.end, baseline: r(l.baseline), width: r(l.width) }));
6085
+ if (layer.rendering === "stored") missing = layout.missingFonts;
6086
+ }
6087
+ const info = {
6088
+ id: layer.id,
6089
+ name: layer.name,
6090
+ text: layer.text,
6091
+ runs: ranges(layer.runs),
6092
+ paragraphs: ranges(layer.paragraphs),
6093
+ frame: { ...layer.frame },
6094
+ transform: [...layer.transform ?? [1, 0, 0, 1, 0, 0]],
6095
+ lines,
6096
+ bounds: this.getLayerBounds(id),
6097
+ missingFonts: [...missing],
6098
+ rendering: layer.rendering === "stored" ? "stored" : "live"
6099
+ };
6100
+ if (layer.annotation) info.annotationId = layer.annotation.id;
6101
+ return info;
6102
+ }
6103
+ /** A type layer's text, runs, paragraphs, frame, lines and bounds. */
6104
+ getTextLayer(id) {
6105
+ return this.typeInfo(id);
6106
+ }
6107
+ /**
6108
+ * Adds a type layer (one undo step) and makes it active: point text at `x`, `y`, or paragraph
6109
+ * text wrapped in `box`. Refused without a text shaper. Returns its id.
6110
+ */
6111
+ addTextLayer(init) {
6112
+ const text = normalizeText(init.text);
6113
+ if (!Number.isFinite(init.x) || !Number.isFinite(init.y)) throw new LayerOperationError("x and y must be numbers (document pixels).");
6114
+ const style = patchTextStyle({ ...DEFAULT_TEXT_STYLE, color: this.state.paint.foreground }, init.style ?? {});
6115
+ const paragraph = patchParagraphStyle({}, init.paragraph ?? {});
6116
+ let frame = { kind: "point" };
6117
+ if (init.box) {
6118
+ const { width, height } = init.box;
6119
+ if (!(Number.isFinite(width) && width >= 1 && Number.isFinite(height) && height >= 1)) throw new LayerOperationError("box.width and box.height must be at least 1 pixel.");
6120
+ frame = { kind: "box", width, height };
6121
+ }
6122
+ if (init.rotation !== void 0 && !Number.isFinite(init.rotation)) throw new LayerOperationError("rotation must be a number of degrees.");
6123
+ if (init.opacity !== void 0 && !(Number.isFinite(init.opacity) && init.opacity >= 0 && init.opacity <= 1)) throw new LayerOperationError("opacity must be between 0 and 1.");
6124
+ if (init.blend !== void 0 && !isBlendMode(init.blend)) throw new LayerOperationError(`Unknown blend mode "${String(init.blend)}".`);
6125
+ const t = [1, 0, 0, 1, init.x, init.y];
6126
+ const transform = init.rotation ? multiplyMatrices(t, rotateMatrix(init.rotation, { x: init.x, y: init.y })) : t;
6127
+ const layer = createTypeLayer({ text, runs: [{ length: text.length, style }], paragraphs: [{ length: text.length, style: paragraph }], frame, transform, name: init.name ?? typeLayerName(text) }, this.textEnv());
6128
+ if (init.opacity !== void 0) layer.opacity = init.opacity;
6129
+ if (init.blend !== void 0) layer.blend = init.blend;
6130
+ const next = placeLayer(this.state.layers, layer, this.newLayerPlacement(init));
6131
+ this.commitLayers(next, () => {
6132
+ this.state.activeLayerId = layer.id;
6133
+ this.state.activeTarget = "pixels";
6134
+ });
6135
+ return layer.id;
6136
+ }
6137
+ /**
6138
+ * Replaces text [start, end) (default all of it) with `text`; inserted text takes the style at
6139
+ * `start`. One undo step unless `commitToHistory` is false (typing). Returns the layer.
6140
+ */
6141
+ setText(id, text, range = {}, commitToHistory = true) {
6142
+ const layer = requireTypeLayer(this.state.layers, id);
6143
+ const insert = normalizeText(text);
6144
+ const [start, end] = textRange(layer.text, range.start, range.end);
6145
+ const { layers } = updateTypeLayer(this.state.layers, id, (l) => replaceTextRange(l, start, end, insert), this.textEnv(), "edit its text");
6146
+ this.commitLayers(layers, void 0, commitToHistory);
6147
+ return this.typeInfo(id);
6148
+ }
6149
+ /** Sets character style fields over [start, end) (default all), one undo step. Returns the layer. */
6150
+ setTextStyle(id, style, range = {}, commitToHistory = true) {
6151
+ const layer = requireTypeLayer(this.state.layers, id);
6152
+ const [start, end] = textRange(layer.text, range.start, range.end);
6153
+ if (!Object.keys(style).length) throw new LayerOperationError("Give at least one style field to set.");
6154
+ patchTextStyle(DEFAULT_TEXT_STYLE, style);
6155
+ const { layers } = updateTypeLayer(
6156
+ this.state.layers,
6157
+ id,
6158
+ (l) => {
6159
+ if (start === end) {
6160
+ if (l.text.length) throw new LayerOperationError("The range is empty; give start < end (or leave both out for the whole text).");
6161
+ return { runs: [{ length: 0, style: patchTextStyle(styleAt(l.runs, 0, DEFAULT_TEXT_STYLE), style) }] };
6162
+ }
6163
+ return { runs: restyleRange(l.runs, start, end, (s) => patchTextStyle(s, style)) };
6164
+ },
6165
+ this.textEnv(),
6166
+ "restyle its text"
6167
+ );
6168
+ this.commitLayers(layers, void 0, commitToHistory);
6169
+ return this.typeInfo(id);
6170
+ }
6171
+ /** Sets paragraph style fields on every paragraph that [start, end) touches (default all), one undo step. */
6172
+ setParagraphStyle(id, style, range = {}, commitToHistory = true) {
6173
+ const layer = requireTypeLayer(this.state.layers, id);
6174
+ const [start, end] = textRange(layer.text, range.start, range.end);
6175
+ if (!Object.keys(style).length) throw new LayerOperationError("Give at least one paragraph field to set.");
6176
+ patchParagraphStyle({}, style);
6177
+ const { layers } = updateTypeLayer(
6178
+ this.state.layers,
6179
+ id,
6180
+ (l) => {
6181
+ let at = 0;
6182
+ const paragraphs = l.paragraphs.map((p) => {
6183
+ const a = at;
6184
+ at += p.length;
6185
+ const touches = a < end && at > start || start === end && start >= a && (start < at || at === l.text.length);
6186
+ return touches || !l.text.length ? { length: p.length, style: patchParagraphStyle(p.style, style) } : p;
6187
+ });
6188
+ return { paragraphs };
6189
+ },
6190
+ this.textEnv(),
6191
+ "restyle its paragraphs"
6192
+ );
6193
+ this.commitLayers(layers, void 0, commitToHistory);
6194
+ return this.typeInfo(id);
6195
+ }
6196
+ /**
6197
+ * Point text ↔ paragraph text: `{ kind: 'box', width, height }` wraps the text in a box (its
6198
+ * top-left where the first line's top-left is now), `{ kind: 'point' }` sets it on one line per
6199
+ * paragraph. Also resizes a box. One undo step.
6200
+ */
6201
+ setTextFrame(id, frame, commitToHistory = true) {
6202
+ const layer = requireTypeLayer(this.state.layers, id);
6203
+ if (frame.kind === "box" && !(Number.isFinite(frame.width) && frame.width >= 1 && Number.isFinite(frame.height) && frame.height >= 1)) throw new LayerOperationError("width and height must be at least 1 pixel.");
6204
+ if (frame.kind !== "box" && frame.kind !== "point") throw new LayerOperationError("kind must be point or box.");
6205
+ const shaper = this.getTextShaper();
6206
+ const { layers } = updateTypeLayer(
6207
+ this.state.layers,
6208
+ id,
6209
+ (l) => {
6210
+ if (frame.kind === "box" && l.frame.kind === "box") return { frame: { kind: "box", width: frame.width, height: frame.height } };
6211
+ if (!shaper) throw new LayerOperationError("Changing between point and paragraph text needs a text shaper to measure the text.");
6212
+ const lines = layoutText(l, shaper).lines;
6213
+ const first = lines[0];
6214
+ const dx = frame.kind === "box" ? first ? first.x : 0 : 0;
6215
+ const dy = frame.kind === "box" ? first ? first.baseline - first.ascent : 0 : first ? first.baseline : 0;
6216
+ const m = textMatrix(l);
6217
+ const shifted = [m[0], m[1], m[2], m[3], m[0] * dx + m[2] * dy + m[4], m[1] * dx + m[3] * dy + m[5]];
6218
+ const nextFrame = frame.kind === "box" ? { kind: "box", width: frame.width, height: frame.height } : { kind: "point" };
6219
+ return { frame: nextFrame, transform: shifted };
6220
+ },
6221
+ this.textEnv(),
6222
+ "change its frame"
6223
+ );
6224
+ this.commitLayers(layers, void 0, commitToHistory);
6225
+ return this.typeInfo(id);
6226
+ }
6227
+ /**
6228
+ * Convert to Shape: the text's glyph outlines become a shape layer with the same id, name and
6229
+ * layer properties (needs a shaper that gives outlines). The fill is the first run's colour;
6230
+ * `warnings` says when other colours were dropped. One undo step.
6231
+ */
6232
+ convertTextToShape(id) {
6233
+ const layer = requireTypeLayer(this.state.layers, id);
6234
+ requireUnlocked(layer, "all", "convert it");
6235
+ const shaper = this.getTextShaper();
6236
+ if (!shaper) throw new LayerOperationError("Convert to Shape needs a text shaper that gives glyph outlines; none is set here.");
6237
+ const parts = textOutlines(layer, shaper);
6238
+ const subpaths = parts.flatMap((p) => bezierToSubpaths(p.path));
6239
+ if (!subpaths.length) throw new LayerOperationError("The text draws nothing (it is empty or only spaces), so there is no shape to make.");
6240
+ const warnings = [];
6241
+ const colors = new Set(layer.runs.filter((r) => r.length > 0).map((r) => `${r.style.color}/${r.style.opacity ?? 1}`));
6242
+ if (colors.size > 1) warnings.push("The text had several colours; the shape takes the first one.");
6243
+ const missing = layoutText(layer, shaper).missingFonts;
6244
+ if (missing.length) warnings.push(`Missing fonts were drawn with a fallback: ${missing.join(", ")}.`);
6245
+ const first = layer.runs.find((r) => r.length > 0)?.style ?? DEFAULT_TEXT_STYLE;
6246
+ const fill = first.opacity !== void 0 && first.opacity !== 1 ? { type: "color", color: first.color, opacity: first.opacity } : { type: "color", color: first.color };
6247
+ const shape = createShapeLayer({ components: [{ op: "combine", subpaths }], fill, stroke: null, name: layer.name }, this.shapeEnv(), layer.id);
6248
+ const keep = { visible: layer.visible, opacity: layer.opacity, blend: layer.blend };
6249
+ for (const k of ["clipped", "mask", "locks", "effects", "fillOpacity"]) if (layer[k] !== void 0) keep[k] = layer[k];
6250
+ this.commitLayers(mapLayer(this.state.layers, id, () => ({ ...shape, ...keep })));
6251
+ return { layerId: id, warnings };
6252
+ }
6253
+ /** The layout of a type layer (lines, characters, bounds in text space), or `null` without a shaper. */
6254
+ getTextLayout(id) {
6255
+ const layer = requireTypeLayer(this.state.layers, id);
6256
+ const shaper = this.getTextShaper();
6257
+ return shaper ? layoutText(layer, shaper) : null;
6258
+ }
6259
+ /** The text index nearest a document point (for clicks on a type layer). */
6260
+ textIndexAt(id, point) {
6261
+ const layer = requireTypeLayer(this.state.layers, id);
6262
+ const layout = this.getTextLayout(id);
6263
+ return layout ? textIndexAt(layout, toTextSpace(layer, point)) : layer.text.length;
6264
+ }
6265
+ /** The caret at text index `index` as a document-space segment (top to bottom), for drawing it. */
6266
+ textCaret(id, index) {
6267
+ const layer = requireTypeLayer(this.state.layers, id);
6268
+ const layout = this.getTextLayout(id);
6269
+ if (!layout) return null;
6270
+ const c = caretAt(layout, Math.max(0, Math.min(layer.text.length, index)));
6271
+ const m = textMatrix(layer);
6272
+ const at = (x, y) => ({ x: m[0] * x + m[2] * y + m[4], y: m[1] * x + m[3] * y + m[5] });
6273
+ return { top: at(c.x, c.top), bottom: at(c.x, c.bottom), line: c.line };
6274
+ }
5784
6275
  // --- Shape layers ---
5785
6276
  /** What shape rendering needs: the canvas size and the document's patterns. */
5786
6277
  shapeEnv() {
@@ -5957,11 +6448,11 @@ var ImageEditorController = class {
5957
6448
  }
5958
6449
  } else if (layer.type === "smart") {
5959
6450
  return this.rasterizeSmartObject(id);
5960
- } else if (layer.type === "shape") {
5961
- next = shapeAsPixelLayer(layer);
6451
+ } else if (layer.type === "shape" || layer.type === "type") {
6452
+ next = layer.type === "shape" ? shapeAsPixelLayer(layer) : typeAsPixelLayer(layer);
5962
6453
  if (this.state.selectedAnnotationId && this.state.selectedAnnotationId === layer.annotation?.id) this.state.selectedAnnotationId = null;
5963
6454
  } else {
5964
- throw new LayerOperationError(`Only shape and annotation layers, smart objects and pixel layers with filters rasterize; "${layer.name || id}" is a ${layer.type} layer. Merge it down or flatten instead.`);
6455
+ throw new LayerOperationError(`Only shape, type and annotation layers, smart objects and pixel layers with filters rasterize; "${layer.name || id}" is a ${layer.type} layer. Merge it down or flatten instead.`);
5965
6456
  }
5966
6457
  this.commitLayers(mapLayer(this.state.layers, id, () => next));
5967
6458
  return id;
@@ -6086,9 +6577,9 @@ var ImageEditorController = class {
6086
6577
  if (mask2) out2.mask = mask2;
6087
6578
  return out2;
6088
6579
  }
6089
- if (layer.type === "shape") {
6090
- add2("Shape layers were written as pixel layers", layer.name);
6091
- const out2 = shapeAsPixelLayer(layer);
6580
+ if (layer.type === "shape" || layer.type === "type") {
6581
+ add2(layer.type === "shape" ? "Shape layers were written as pixel layers" : "Type layers were written as pixel layers", layer.name);
6582
+ const out2 = layer.type === "shape" ? shapeAsPixelLayer(layer) : typeAsPixelLayer(layer);
6092
6583
  const mask2 = bakeMask(layer);
6093
6584
  if (mask2) out2.mask = mask2;
6094
6585
  else delete out2.mask;
@@ -6190,7 +6681,7 @@ var ImageEditorController = class {
6190
6681
  const bg = developTarget(layers, null);
6191
6682
  if (bg && bg.source !== "document") layers = mapLayer(layers, bg.id, () => ({ ...bg, source: "document" }));
6192
6683
  if (clearAnnotations) {
6193
- for (const l of flattenLayers(layers)) if (l.type === "annotation" || l.type === "shape" && l.annotation) layers = mapLayer(layers, l.id, () => null);
6684
+ for (const l of flattenLayers(layers)) if (l.type === "annotation" || (l.type === "shape" || l.type === "type") && l.annotation) layers = mapLayer(layers, l.id, () => null);
6194
6685
  this.state.selectedAnnotationId = null;
6195
6686
  }
6196
6687
  this.state.layers = layers;
@@ -6266,7 +6757,8 @@ var ImageEditorController = class {
6266
6757
  problems,
6267
6758
  smartContents: Array.isArray(data.smartContents) ? data.smartContents : [],
6268
6759
  canvas,
6269
- pattern: (pid) => storedPatterns.find((p) => p.id === pid) ?? null
6760
+ pattern: (pid) => storedPatterns.find((p) => p.id === pid) ?? null,
6761
+ text: this.textEnv()
6270
6762
  });
6271
6763
  this.lastLoadProblems = problems;
6272
6764
  s.crop = data.crop ? { ...data.crop } : null;
@@ -6469,7 +6961,7 @@ function collectStateTiles(layers, into) {
6469
6961
  for (const l of flattenLayers(layers)) {
6470
6962
  if (l.type === "pixel" && l.source !== "document") collectTiles(l.surface, into);
6471
6963
  if (l.mask && !l.mask.source) collectTiles(l.mask.surface, into);
6472
- if (l.type === "shape") collectTiles(l.surface, into);
6964
+ if (l.type === "shape" || l.type === "type") collectTiles(l.surface, into);
6473
6965
  if (l.type === "smart") {
6474
6966
  collectTiles(l.surface, into);
6475
6967
  if (l.smartFilters?.mask) collectTiles(l.smartFilters.mask.surface, into);
@@ -6518,7 +7010,7 @@ function resizePixels(px2, width, height) {
6518
7010
  function withoutAnnotations(layers) {
6519
7011
  const out = [];
6520
7012
  for (const l of layers) {
6521
- if (l.type === "annotation" || l.type === "shape" && l.annotation) continue;
7013
+ if (l.type === "annotation" || (l.type === "shape" || l.type === "type") && l.annotation) continue;
6522
7014
  out.push(l.type === "group" ? { ...l, children: withoutAnnotations(l.children) } : l);
6523
7015
  }
6524
7016
  return out;
@@ -6708,7 +7200,7 @@ function loadImageEditorDocument(input) {
6708
7200
  if (state.selection === void 0) delete state.selection;
6709
7201
  return { state, report: result.report, unknown: result.unknown };
6710
7202
  }
6711
- var LAYER_TYPES = /* @__PURE__ */ new Set(["pixel", "adjustment", "group", "annotation", "smart", "shape"]);
7203
+ var LAYER_TYPES = /* @__PURE__ */ new Set(["pixel", "adjustment", "group", "annotation", "smart", "shape", "type"]);
6712
7204
  function cleanLayers(value, dropped) {
6713
7205
  const out = [];
6714
7206
  for (const raw of value) {
@@ -7768,6 +8260,18 @@ function describeLayer(layer, activeId, boundsOf) {
7768
8260
  };
7769
8261
  if (layer.annotation) out.annotation = { id: layer.annotation.id, type: layer.annotation.type };
7770
8262
  break;
8263
+ case "type":
8264
+ out.bounds = boundsOf(layer);
8265
+ out.fillOpacity = round3(layer.fillOpacity ?? 1);
8266
+ out.text = {
8267
+ text: layer.text.length > 80 ? `${layer.text.slice(0, 80)}\u2026` : layer.text,
8268
+ frame: layer.frame.kind,
8269
+ runs: layer.runs.length,
8270
+ rendering: layer.rendering === "stored" ? "stored" : "live",
8271
+ missingFonts: layer.missingFonts ?? []
8272
+ };
8273
+ if (layer.annotation) out.annotation = { id: layer.annotation.id, type: layer.annotation.type };
8274
+ break;
7771
8275
  case "smart":
7772
8276
  out.bounds = boundsOf(layer);
7773
8277
  out.fillOpacity = round3(layer.fillOpacity ?? 1);
@@ -7813,7 +8317,7 @@ function createImageLayerTools(controller) {
7813
8317
  const listLayers = {
7814
8318
  name: "list_layers",
7815
8319
  title: "List image layers",
7816
- description: "Returns the layer stack bottom to top as a tree: id, type (pixel, adjustment, group, shape, smart, annotation), name, document bounds (pixel, shape and smart layers), visible, opacity, fillOpacity, blend, clipped, locks, a mask summary, an effects summary (master switch, scale, effect ids and kinds; details from list_layer_effects), filters (pixel layers), kind and params (adjustment layers), a shape summary (component operations and kinds, fill and stroke paint types; details from get_shape_layer), children (groups) and which layer is active. Also returns the active layer id and target (pixels or mask) and the canvas size.",
8320
+ description: "Returns the layer stack bottom to top as a tree: id, type (pixel, adjustment, group, shape, type, smart, annotation), name, document bounds (pixel, shape, type and smart layers), visible, opacity, fillOpacity, blend, clipped, locks, a mask summary, an effects summary (master switch, scale, effect ids and kinds; details from list_layer_effects), filters (pixel layers), kind and params (adjustment layers), a shape summary (component operations and kinds, fill and stroke paint types; details from get_shape_layer), a text summary (the text, point or box frame, run count, live or stored pixels, missing fonts; details from get_text_layer), children (groups) and which layer is active. Also returns the active layer id and target (pixels or mask) and the canvas size.",
7817
8321
  inputSchema: { type: "object", properties: {}, additionalProperties: false },
7818
8322
  annotations: { title: "List image layers", ...READ2 },
7819
8323
  handler: () => {
@@ -8157,7 +8661,7 @@ function createImageLayerTools(controller) {
8157
8661
  const rasterize = {
8158
8662
  name: "rasterize_layer",
8159
8663
  title: "Rasterize a layer",
8160
- description: "Rasterize Layer: turns a shape layer into a pixel layer holding what it shows now (same id, name, opacity, blend, fill, mask and effects; its paths are gone, so later transforms resample pixels), turns an annotation layer into a pixel layer (needs a browser), rasterizes a smart object, or bakes a pixel layer's filters (develop, blur) into its pixels. One undo step. Returns the layer.",
8664
+ description: "Rasterize Layer: turns a shape or type layer into a pixel layer holding what it shows now (same id, name, opacity, blend, fill, mask and effects; its paths are gone, so later transforms resample pixels), turns an annotation layer into a pixel layer (needs a browser), rasterizes a smart object, or bakes a pixel layer's filters (develop, blur) into its pixels. One undo step. Returns the layer.",
8161
8665
  inputSchema: { type: "object", properties: { layerId: LAYER_ID2 }, required: ["layerId"], additionalProperties: false },
8162
8666
  annotations: { title: "Rasterize a layer", ...DESTRUCTIVE },
8163
8667
  handler: async ({ layerId }) => ({ layer: describe(await controller.rasterizeLayer(exists(layerId))) })
@@ -10006,6 +10510,176 @@ function createImageShapeTools(controller) {
10006
10510
  return [get, add2, update, setComponent, addComponent, removeComponent, merge];
10007
10511
  }
10008
10512
 
10513
+ // src/image-editor/type-tools.ts
10514
+ var READ9 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
10515
+ var WRITE8 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
10516
+ var WRITE_IDEMPOTENT6 = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
10517
+ var DESTRUCTIVE7 = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
10518
+ var LAYER_ID6 = { type: "string", description: "Type layer id from list_layers (type type) or add_text_layer." };
10519
+ var START = { type: "integer", minimum: 0, description: "Start of the range in UTF-16 code units, as get_text_layer counts them (default 0)." };
10520
+ var END = { type: "integer", minimum: 0, description: "End of the range, exclusive (default the end of the text)." };
10521
+ var STYLE = {
10522
+ fontFamily: { type: "string", description: "Font family name (list_fonts shows what is available; a missing family falls back, with a warning)." },
10523
+ fontWeight: { type: "integer", minimum: 1, maximum: 1e3, description: "Weight 100..900 (400 regular, 700 bold)." },
10524
+ italic: { type: "boolean", description: "Italic." },
10525
+ fontSize: { type: "number", minimum: 0.5, maximum: 5e3, description: "Size in document pixels." },
10526
+ color: { type: "string", description: "Colour (#rrggbb, or a CSS colour; its alpha becomes opacity)." },
10527
+ opacity: { type: ["number", "null"], minimum: 0, maximum: 1, description: "Text opacity 0..1 (null resets to 1)." },
10528
+ tracking: { type: ["number", "null"], minimum: -1e3, maximum: 1e4, description: "Letter spacing in 1/1000 em (null resets to 0)." },
10529
+ leading: { type: ["number", "null"], minimum: 0, maximum: 1e4, description: "Baseline-to-baseline distance in pixels (null = auto, the paragraph autoLeading \xD7 size)." },
10530
+ baselineShift: { type: ["number", "null"], minimum: -5e3, maximum: 5e3, description: "Pixels; positive raises the text (null resets to 0)." },
10531
+ underline: { type: "boolean", description: "Underline." },
10532
+ strikethrough: { type: "boolean", description: "Strike through." }
10533
+ };
10534
+ var PARAGRAPH = {
10535
+ align: { type: "string", enum: ["left", "center", "right", "justify"], description: "Alignment (justify stretches paragraph-box lines but the last)." },
10536
+ autoLeading: { type: ["number", "null"], minimum: 0.1, maximum: 10, description: "Auto leading as a multiple of the font size (default 1.2)." },
10537
+ firstLineIndent: { type: ["number", "null"], description: "First-line indent in pixels." },
10538
+ startIndent: { type: ["number", "null"], description: "Left indent in pixels." },
10539
+ endIndent: { type: ["number", "null"], description: "Right indent in pixels." },
10540
+ spaceBefore: { type: ["number", "null"], description: "Space above the paragraph in pixels (not before the first)." },
10541
+ spaceAfter: { type: ["number", "null"], description: "Space below the paragraph in pixels." }
10542
+ };
10543
+ function pick(args, keys) {
10544
+ const out = {};
10545
+ for (const k of keys) if (args[k] !== void 0) out[k] = args[k];
10546
+ return out;
10547
+ }
10548
+ function createImageTypeTools(controller) {
10549
+ const typeId = (id) => {
10550
+ if (typeof id !== "string" || !id) throw new LayerOperationError("layerId is required (a type layer id from list_layers).");
10551
+ requireTypeLayer(controller.getLayers(), id);
10552
+ return id;
10553
+ };
10554
+ const layerSummary = (id) => {
10555
+ const s = controller.getState();
10556
+ const loc = findLayer(s.layers, id);
10557
+ return loc ? { ...describeLayer(loc.layer, s.activeLayerId, (x) => controller.getLayerBounds(x.id)), parentId: loc.parent?.id ?? null, index: loc.index } : null;
10558
+ };
10559
+ const range = (args) => ({ start: args.start, end: args.end });
10560
+ const listFonts = {
10561
+ name: "list_fonts",
10562
+ title: "List fonts",
10563
+ description: "Returns the font families type layers can use here (generic families such as sans-serif always work). Changes nothing.",
10564
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
10565
+ annotations: { title: "List fonts", ...READ9 },
10566
+ handler: () => ({ families: controller.listFonts(), shaper: controller.getTextShaper()?.id ?? null })
10567
+ };
10568
+ const get = {
10569
+ name: "get_text_layer",
10570
+ title: "Read a type layer",
10571
+ description: "Returns a type layer\u2019s text, its character runs and paragraphs as [start, end) ranges in UTF-16 code units with their styles, the frame (point text, or a paragraph box with its size), the text-to-document transform, the laid-out lines (range, baseline, width), the rendered bounds, missing fonts and whether the pixels are live or stored from a file. Call before editing text you did not write, to get the ranges. Changes nothing.",
10572
+ inputSchema: { type: "object", properties: { layerId: LAYER_ID6 }, required: ["layerId"], additionalProperties: false },
10573
+ annotations: { title: "Read a type layer", ...READ9 },
10574
+ handler: ({ layerId }) => controller.getTextLayer(typeId(layerId))
10575
+ };
10576
+ const add2 = {
10577
+ name: "add_text_layer",
10578
+ title: "Add a type layer",
10579
+ description: "Adds a live type layer and makes it active: point text whose origin (x, y) is the first line\u2019s baseline at its alignment edge, or paragraph text wrapped in box {width, height} with its top-left at (x, y). Line breaks in text start paragraphs. Style and paragraph settings apply to the whole text (change ranges later with set_text_style). Default style: 24 px sans-serif in the foreground colour. One undo step. Returns the layer and the text layer, including missingFonts.",
10580
+ inputSchema: {
10581
+ type: "object",
10582
+ properties: {
10583
+ text: { type: "string", description: "The text; \\n starts a new paragraph." },
10584
+ x: { type: "number", description: "Document x of the origin, pixels." },
10585
+ y: { type: "number", description: "Document y of the origin (point text: the first baseline), pixels." },
10586
+ box: {
10587
+ type: "object",
10588
+ description: "Make it paragraph (area) text wrapped in this box, pixels.",
10589
+ properties: { width: { type: "number", minimum: 1, description: "Box width, pixels." }, height: { type: "number", minimum: 1, description: "Box height, pixels." } },
10590
+ required: ["width", "height"],
10591
+ additionalProperties: false
10592
+ },
10593
+ style: { type: "object", description: "Character style.", properties: STYLE, additionalProperties: false },
10594
+ paragraph: { type: "object", description: "Paragraph style.", properties: PARAGRAPH, additionalProperties: false },
10595
+ rotation: { type: "number", description: "Degrees clockwise about the origin (default 0)." },
10596
+ name: { type: "string", description: "Layer name (default the start of the text)." },
10597
+ opacity: { type: "number", minimum: 0, maximum: 1, description: "Layer opacity 0..1 (default 1)." },
10598
+ blend: { type: "string", enum: BLEND_MODES.map((m) => m.id), description: "Blend mode (default normal)." },
10599
+ aboveId: { type: "string", description: "Put the layer directly above this layer (default above the active layer)." },
10600
+ belowId: { type: "string", description: "Put the layer directly below this layer." },
10601
+ parentId: { type: "string", description: "Put the layer inside this group." },
10602
+ index: { type: "integer", minimum: 0, description: "Position among the siblings, 0 = bottom." }
10603
+ },
10604
+ required: ["text", "x", "y"],
10605
+ additionalProperties: false
10606
+ },
10607
+ annotations: { title: "Add a type layer", ...WRITE8 },
10608
+ handler: (args) => {
10609
+ const init = pick(args, ["text", "x", "y", "box", "style", "paragraph", "rotation", "name", "opacity", "blend", "aboveId", "belowId", "parentId", "index"]);
10610
+ if (init.blend !== void 0) init.blend = init.blend;
10611
+ const id = controller.addTextLayer(init);
10612
+ return { layer: layerSummary(id), text: controller.getTextLayer(id) };
10613
+ }
10614
+ };
10615
+ const setText = {
10616
+ name: "set_text",
10617
+ title: "Edit text",
10618
+ description: "Replaces the text in [start, end) (default all of it) with text; inserted text takes the style of the character at start. Use start = end to insert. Returns the text layer with its new runs.",
10619
+ inputSchema: { type: "object", properties: { layerId: LAYER_ID6, text: { type: "string", description: "Replacement text (\\n starts a paragraph; empty deletes the range)." }, start: START, end: END }, required: ["layerId", "text"], additionalProperties: false },
10620
+ annotations: { title: "Edit text", ...WRITE8 },
10621
+ handler: (args) => controller.setText(typeId(args.layerId), args.text, range(args))
10622
+ };
10623
+ const setStyle = {
10624
+ name: "set_text_style",
10625
+ title: "Style a range of text",
10626
+ description: "Sets character style fields (font, weight, italic, size, colour, tracking, leading, baseline shift, underline, strike-through) over [start, end) of a type layer (default all of it); fields not given keep their values per run. Returns the text layer with its runs and missingFonts.",
10627
+ inputSchema: { type: "object", properties: { layerId: LAYER_ID6, start: START, end: END, ...STYLE }, required: ["layerId"], additionalProperties: false },
10628
+ annotations: { title: "Style a range of text", ...WRITE_IDEMPOTENT6 },
10629
+ handler: (args) => {
10630
+ const style = pick(args, Object.keys(STYLE));
10631
+ if (!Object.keys(style).length) throw new LayerOperationError(`Give at least one style field: ${Object.keys(STYLE).join(", ")}.`);
10632
+ return controller.setTextStyle(typeId(args.layerId), style, range(args));
10633
+ }
10634
+ };
10635
+ const setParagraph = {
10636
+ name: "set_paragraph_style",
10637
+ title: "Style paragraphs",
10638
+ description: "Sets paragraph fields (alignment, auto leading, indents, spacing) on every paragraph that [start, end) touches (default all). Returns the text layer.",
10639
+ inputSchema: { type: "object", properties: { layerId: LAYER_ID6, start: START, end: END, ...PARAGRAPH }, required: ["layerId"], additionalProperties: false },
10640
+ annotations: { title: "Style paragraphs", ...WRITE_IDEMPOTENT6 },
10641
+ handler: (args) => {
10642
+ const style = pick(args, Object.keys(PARAGRAPH));
10643
+ if (!Object.keys(style).length) throw new LayerOperationError(`Give at least one paragraph field: ${Object.keys(PARAGRAPH).join(", ")}.`);
10644
+ return controller.setParagraphStyle(typeId(args.layerId), style, range(args));
10645
+ }
10646
+ };
10647
+ const setFrame = {
10648
+ name: "set_text_frame",
10649
+ title: "Point or paragraph text",
10650
+ description: "Converts between point text (kind point: one line per paragraph) and paragraph text (kind box: wrapped in width \xD7 height pixels, its top-left where the first line starts), or resizes a paragraph box. The first line stays in place. Returns the text layer with its new lines.",
10651
+ inputSchema: {
10652
+ type: "object",
10653
+ properties: {
10654
+ layerId: LAYER_ID6,
10655
+ kind: { type: "string", enum: ["point", "box"], description: "point or box." },
10656
+ width: { type: "number", minimum: 1, description: "Box width in pixels (kind box)." },
10657
+ height: { type: "number", minimum: 1, description: "Box height in pixels (kind box)." }
10658
+ },
10659
+ required: ["layerId", "kind"],
10660
+ additionalProperties: false
10661
+ },
10662
+ annotations: { title: "Point or paragraph text", ...WRITE_IDEMPOTENT6 },
10663
+ handler: ({ layerId, kind, width, height }) => {
10664
+ const id = typeId(layerId);
10665
+ if (kind === "box" && (width === void 0 || height === void 0)) throw new LayerOperationError("kind box needs width and height in pixels.");
10666
+ return controller.setTextFrame(id, kind === "box" ? { kind: "box", width, height } : { kind: "point" });
10667
+ }
10668
+ };
10669
+ const toShape = {
10670
+ name: "convert_text_to_shape",
10671
+ title: "Convert text to a shape",
10672
+ description: "Type \u203A Convert to Shape: the text\u2019s glyph outlines become a shape layer (same id, name, mask, effects, opacity, blend) that can be edited with the shape tools; the text is no longer editable. Needs a font shaper that gives outlines (the browser\u2019s canvas cannot). The fill is the first run\u2019s colour. One undo step. Returns the layer and warnings.",
10673
+ inputSchema: { type: "object", properties: { layerId: LAYER_ID6 }, required: ["layerId"], additionalProperties: false },
10674
+ annotations: { title: "Convert text to a shape", ...DESTRUCTIVE7 },
10675
+ handler: ({ layerId }) => {
10676
+ const { layerId: id, warnings } = controller.convertTextToShape(typeId(layerId));
10677
+ return { layer: layerSummary(id), shape: controller.getShapeLayer(id), warnings };
10678
+ }
10679
+ };
10680
+ return [listFonts, get, add2, setText, setStyle, setParagraph, setFrame, toShape];
10681
+ }
10682
+
10009
10683
  // src/image-editor/tools.ts
10010
10684
  var LAYER_PATH = /^layers\.([^.]+)\.(.+)$/;
10011
10685
  function adjustmentsSummary(adjustments) {
@@ -10246,6 +10920,7 @@ function createImageEditorToolProvider(controller, options = {}) {
10246
10920
  ...createImageTransformTools(controller),
10247
10921
  ...createImageSmartObjectTools(controller),
10248
10922
  ...createImageShapeTools(controller),
10923
+ ...createImageTypeTools(controller),
10249
10924
  ...createImageEffectTools(controller),
10250
10925
  ...createImagePaintTools(controller),
10251
10926
  ...createPigmentTools(),
@@ -10588,6 +11263,8 @@ function layerBadge(layer) {
10588
11263
  return "so";
10589
11264
  case "shape":
10590
11265
  return "shp";
11266
+ case "type":
11267
+ return "T";
10591
11268
  }
10592
11269
  }
10593
11270
  function dropZone(fraction, isGroup) {
@@ -10627,7 +11304,7 @@ function layerMenu(controller) {
10627
11304
  { action: "delete-mask", label: "Delete Layer Mask", enabled: Boolean(active?.mask) },
10628
11305
  { action: "toggle-mask", label: active?.mask?.enabled === false ? "Enable Layer Mask" : "Disable Layer Mask", enabled: Boolean(active?.mask) },
10629
11306
  { action: "toggle-mask-link", label: active?.mask?.linked === false ? "Link Layer Mask" : "Unlink Layer Mask", enabled: Boolean(active?.mask && !active.mask.source) },
10630
- { action: "rasterize", label: "Rasterize Layer", enabled: active?.type === "annotation" || active?.type === "smart" || active?.type === "shape" || isPixel && Boolean(active?.type === "pixel" && active.filters?.length) },
11307
+ { action: "rasterize", label: "Rasterize Layer", enabled: active?.type === "annotation" || active?.type === "smart" || active?.type === "shape" || active?.type === "type" || isPixel && Boolean(active?.type === "pixel" && active.filters?.length) },
10631
11308
  { action: "convert-smart", label: "Convert to Smart Object", enabled: Boolean(active && active.type !== "annotation") },
10632
11309
  { action: "edit-contents", label: "Edit Contents", enabled: active?.type === "smart" },
10633
11310
  { action: "smart-via-copy", label: "New Smart Object via Copy", enabled: active?.type === "smart" },
@@ -11560,6 +12237,8 @@ export {
11560
12237
  D50,
11561
12238
  D65,
11562
12239
  DEFAULT_ADJUSTMENTS,
12240
+ DEFAULT_AUTO_LEADING,
12241
+ DEFAULT_BLENDER,
11563
12242
  DEFAULT_BRUSH,
11564
12243
  DEFAULT_DEPTH_MASK,
11565
12244
  DEFAULT_GLOBAL_LIGHT,
@@ -11567,6 +12246,8 @@ export {
11567
12246
  DEFAULT_MIXER,
11568
12247
  DEFAULT_REFINE_EDGE_PARAMS,
11569
12248
  DEFAULT_SELECTION_TOOL_OPTIONS,
12249
+ DEFAULT_SMUDGE,
12250
+ DEFAULT_TEXT_STYLE,
11570
12251
  DEFAULT_TRANSFORM,
11571
12252
  DEPTH_MASK_CONTROLS,
11572
12253
  DEVELOP_FILTER_ID,
@@ -11616,6 +12297,7 @@ export {
11616
12297
  MixerStroke,
11617
12298
  PAINTERLY_BLEND_MODES,
11618
12299
  PAINT_TOOLS,
12300
+ PARAGRAPH_STYLE_FIELDS,
11619
12301
  PATTERN_BLOCK_KEYS,
11620
12302
  PIGMENT_BANDS,
11621
12303
  PIGMENT_LAMBDA_MIN,
@@ -11623,6 +12305,7 @@ export {
11623
12305
  PIGMENT_WAVELENGTHS,
11624
12306
  PSD_BLEND_KEYS,
11625
12307
  PSD_FILE_ACCEPT,
12308
+ PaintExchange,
11626
12309
  PaintGesture,
11627
12310
  PaintStrokeSession,
11628
12311
  PatternPlacement,
@@ -11639,14 +12322,19 @@ export {
11639
12322
  SMART_RENDER_MARGIN,
11640
12323
  SUPPORTED_ADJUSTMENT_KEYS,
11641
12324
  SelectionGesture,
12325
+ SmudgeTip,
11642
12326
  StrokeCoverage,
11643
12327
  SurfaceDraft,
12328
+ TEXT_ALIGNS,
12329
+ TEXT_RENDER_MARGIN,
12330
+ TEXT_STYLE_FIELDS,
11644
12331
  TILE,
11645
12332
  TILE_SHIFT,
11646
12333
  TINT_SCALE,
11647
12334
  TONE_LUT_SIZE,
11648
12335
  TONE_RANGE_PRESETS,
11649
12336
  TRANSFORM_ANCHORS,
12337
+ TileMedium,
11650
12338
  TiledSurface,
11651
12339
  TransformTooLargeError,
11652
12340
  VIBRANCE_GREY,
@@ -11673,6 +12361,7 @@ export {
11673
12361
  annotationLayerName,
11674
12362
  annotationOfLayer,
11675
12363
  annotationOfShape,
12364
+ annotationOfType,
11676
12365
  applyAdjustmentLayers,
11677
12366
  applyAdjustments,
11678
12367
  applyBoxBlur,
@@ -11696,8 +12385,10 @@ export {
11696
12385
  blendImageData,
11697
12386
  blendLum,
11698
12387
  blendPixel,
12388
+ blendRadius,
11699
12389
  blendRgb,
11700
12390
  blendTile,
12391
+ blenderDab,
11701
12392
  blurFilter,
11702
12393
  borderCoverage,
11703
12394
  boxMean,
@@ -11713,6 +12404,7 @@ export {
11713
12404
  calculateCropDrag,
11714
12405
  canvasCompositeOperation,
11715
12406
  canvasToImagePoint,
12407
+ caretAt,
11716
12408
  changedTiles,
11717
12409
  clampControlValue,
11718
12410
  clampCropToImage,
@@ -11739,6 +12431,8 @@ export {
11739
12431
  coverageToByte,
11740
12432
  coveringRect,
11741
12433
  createAdjustmentLayer,
12434
+ createBlenderScratch,
12435
+ createCanvasTextShaper,
11742
12436
  createImageCaptureSurface,
11743
12437
  createImageEditTools,
11744
12438
  createImageEditorToolProvider,
@@ -11750,10 +12444,15 @@ export {
11750
12444
  createImageShapeTools,
11751
12445
  createImageSmartObjectTools,
11752
12446
  createImageTransformTools,
12447
+ createImageTypeTools,
12448
+ createOutlineTextShaper,
11753
12449
  createPigment,
11754
12450
  createPigmentTools,
11755
12451
  createSelectionMask,
11756
12452
  createShapeLayer,
12453
+ createTypeLayer,
12454
+ cssFont,
12455
+ cssFontFamily,
11757
12456
  curveLut,
11758
12457
  curveLutFromLevels,
11759
12458
  curvePointToBox,
@@ -11811,6 +12510,7 @@ export {
11811
12510
  encodeSurface,
11812
12511
  evalWarpMesh,
11813
12512
  evaluateAdjustmentLayers,
12513
+ exchangeRates,
11814
12514
  expandCoverage,
11815
12515
  exportPsd,
11816
12516
  extractNormalizedLayerItems,
@@ -11821,9 +12521,12 @@ export {
11821
12521
  findLayer,
11822
12522
  findLayerControl,
11823
12523
  finishTile,
12524
+ fitParagraphRuns,
12525
+ fitRuns,
11824
12526
  flattenLayers,
11825
12527
  flattenPath,
11826
12528
  flipCorners,
12529
+ fontOf,
11827
12530
  formatControlValue,
11828
12531
  fullSelection,
11829
12532
  gaussianBlur,
@@ -11914,6 +12617,7 @@ export {
11914
12617
  layerTree,
11915
12618
  layerTypeLabel,
11916
12619
  layersFromLegacy,
12620
+ layoutText,
11917
12621
  legacyAdjustmentLayer,
11918
12622
  legacyMaskOf,
11919
12623
  legacyView,
@@ -11941,6 +12645,7 @@ export {
11941
12645
  mixPigmentRgb,
11942
12646
  mixPigments,
11943
12647
  mixerLoadResult,
12648
+ mixerRates,
11944
12649
  modifySelection,
11945
12650
  moveCurvePoint,
11946
12651
  moveLayer,
@@ -11955,6 +12660,7 @@ export {
11955
12660
  normalizeAdjustmentLayers,
11956
12661
  normalizeAngle,
11957
12662
  normalizeBlendMode,
12663
+ normalizeBlender,
11958
12664
  normalizeBrush,
11959
12665
  normalizeFilter,
11960
12666
  normalizeLayerEffects,
@@ -11968,13 +12674,16 @@ export {
11968
12674
  normalizeShapePaint,
11969
12675
  normalizeShapeStroke,
11970
12676
  normalizeSmartFilter,
12677
+ normalizeSmudge,
11971
12678
  normalizeStoredAdjustments,
11972
12679
  normalizeSubpaths,
12680
+ normalizeText,
11973
12681
  offsetLayer,
11974
12682
  offsetShape,
11975
12683
  offsetSmartObject,
11976
12684
  openPsdFile,
11977
12685
  packRgba,
12686
+ paragraphRanges,
11978
12687
  parseAdjustmentBlock,
11979
12688
  parseBlend,
11980
12689
  parseColor,
@@ -11986,7 +12695,10 @@ export {
11986
12695
  parsePatternBlock,
11987
12696
  parseSelectionMode,
11988
12697
  parseVersionedDescriptor,
12698
+ patchParagraphStyle,
12699
+ patchTextStyle,
11989
12700
  photoFilterMatrix,
12701
+ pigment8,
11990
12702
  pixelLayerBounds,
11991
12703
  pixelLayers,
11992
12704
  placeContents,
@@ -12040,19 +12752,24 @@ export {
12040
12752
  renderShape,
12041
12753
  renderShapeRect,
12042
12754
  renderSmartObject,
12755
+ renderText,
12043
12756
  replaceContents,
12757
+ replaceTextRange,
12044
12758
  requireLayer,
12045
12759
  requireOperation,
12046
12760
  requireShapeLayer,
12047
12761
  requireSmartObject,
12762
+ requireTypeLayer,
12048
12763
  requireUnlocked,
12049
12764
  rerender,
12050
12765
  rerenderShape,
12766
+ rerenderText,
12051
12767
  resizePixels,
12052
12768
  resizeSelection,
12053
12769
  resizeWarpMesh,
12054
12770
  resolveExternalAssets,
12055
12771
  resolveMosaicRegionBounds,
12772
+ restyleRange,
12056
12773
  rgbToGray,
12057
12774
  rgbToHsl,
12058
12775
  rgbToReflectance,
@@ -12099,16 +12816,22 @@ export {
12099
12816
  smoothCoverage,
12100
12817
  smoothPoints,
12101
12818
  solidSurface,
12819
+ spliceRuns,
12102
12820
  splitOverlayAnnotations,
12103
12821
  srgb8ToLab,
12104
12822
  srgbToLinear,
12105
12823
  standardBlendMode,
12824
+ styleAt,
12106
12825
  sumHistograms,
12107
12826
  surfaceBytes,
12108
12827
  surfaceFromPixels,
12109
12828
  surfacesEqual,
12110
12829
  tempTintToXy,
12111
12830
  temperatureToKelvin,
12831
+ textIndexAt,
12832
+ textMatrix,
12833
+ textOutlines,
12834
+ textRange,
12112
12835
  tileCoord,
12113
12836
  tileKey,
12114
12837
  tileRect,
@@ -12117,6 +12840,7 @@ export {
12117
12840
  tipReach,
12118
12841
  tipShape,
12119
12842
  toHex,
12843
+ toTextSpace,
12120
12844
  toneCurve,
12121
12845
  toneLuts,
12122
12846
  toneRange,
@@ -12124,10 +12848,15 @@ export {
12124
12848
  transformMap,
12125
12849
  transformShapeComponents,
12126
12850
  transformSurface,
12851
+ transformTypeLayer,
12127
12852
  transformedBounds,
12128
12853
  transformedBox,
12129
12854
  transformedPoint,
12130
12855
  translateSurface,
12856
+ typeAsPixelLayer,
12857
+ typeFrame,
12858
+ typeFromAnnotation,
12859
+ typeLayerName,
12131
12860
  ungroupLayer,
12132
12861
  uniformTile,
12133
12862
  unionRect,
@@ -12137,6 +12866,7 @@ export {
12137
12866
  updateLayerMaskProps,
12138
12867
  updateShapeLayer,
12139
12868
  updateSmartFilter,
12869
+ updateTypeLayer,
12140
12870
  uvToXy,
12141
12871
  validateEffectGradient,
12142
12872
  validateLayerEffect,