@pixerate/editor 0.110.0 → 0.112.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.
@@ -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-DMRSJBJF.js";
372
+ } from "../chunk-CZOXTEMN.js";
338
373
  import {
339
374
  DEFAULT_GLOBAL_LIGHT,
340
375
  DIRECT_GAUSSIAN_MAX_SIGMA,
@@ -506,12 +541,15 @@ import {
506
541
  WARP_STYLES,
507
542
  WARP_STYLE_LABELS,
508
543
  applyMatrix,
544
+ bezierToSubpaths,
509
545
  dragWarpMeshPoint,
510
546
  evalWarpMesh,
511
547
  identityWarpMesh,
548
+ invertMatrix,
512
549
  multiplyMatrices,
513
550
  pullWarpMesh,
514
551
  resizeWarpMesh,
552
+ rotateMatrix,
515
553
  roundSubpaths,
516
554
  translateMatrix,
517
555
  warpMeshColumns,
@@ -700,7 +738,7 @@ function needsLegacyBase(input) {
700
738
  if (hasResampledLegacyMask(stack.layers, canvas.width, canvas.height)) return true;
701
739
  return false;
702
740
  }
703
- 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;
704
742
  var overlayViews = /* @__PURE__ */ new WeakMap();
705
743
  function overlayView(l) {
706
744
  if (l.type === "annotation") return l;
@@ -716,7 +754,7 @@ function splitOverlayAnnotations(layers) {
716
754
  let i = layers.length;
717
755
  while (i > 0) {
718
756
  const l = layers[i - 1];
719
- 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--;
720
758
  else break;
721
759
  }
722
760
  return {
@@ -1027,6 +1065,45 @@ var ImageEditorRenderer = class {
1027
1065
  }
1028
1066
  ctx.restore();
1029
1067
  }
1068
+ if (options.textEdit) {
1069
+ const t = options.textEdit;
1070
+ ctx.save();
1071
+ try {
1072
+ const scr = (p) => projectImagePoint(p, projection);
1073
+ const poly = (pts) => {
1074
+ ctx.beginPath();
1075
+ pts.map(scr).forEach((p, i) => i ? ctx.lineTo(p.x, p.y) : ctx.moveTo(p.x, p.y));
1076
+ ctx.closePath();
1077
+ };
1078
+ if (t.frame) {
1079
+ poly(t.frame);
1080
+ ctx.setLineDash([4, 3]);
1081
+ ctx.lineWidth = 1;
1082
+ ctx.strokeStyle = "rgba(99,102,241,0.9)";
1083
+ ctx.stroke();
1084
+ ctx.setLineDash([]);
1085
+ }
1086
+ ctx.fillStyle = "rgba(99,102,241,0.35)";
1087
+ for (const q of t.selection) {
1088
+ poly(q);
1089
+ ctx.fill();
1090
+ }
1091
+ if (t.caret) {
1092
+ const [a, b] = t.caret.map(scr);
1093
+ ctx.beginPath();
1094
+ ctx.moveTo(a.x, a.y);
1095
+ ctx.lineTo(b.x, b.y);
1096
+ ctx.lineWidth = 3;
1097
+ ctx.strokeStyle = "#ffffff";
1098
+ ctx.stroke();
1099
+ ctx.lineWidth = 1.5;
1100
+ ctx.strokeStyle = "#111827";
1101
+ ctx.stroke();
1102
+ }
1103
+ } catch {
1104
+ }
1105
+ ctx.restore();
1106
+ }
1030
1107
  if (options.freeTransform) {
1031
1108
  const t = options.freeTransform;
1032
1109
  ctx.save();
@@ -2441,6 +2518,159 @@ function refineSelection(pixels, mask, params = {}) {
2441
2518
  return createSelectionMask(mask.width, mask.height, out);
2442
2519
  }
2443
2520
 
2521
+ // src/image-editor/layers/text-shapers.ts
2522
+ 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"]);
2523
+ function cssFontFamily(family) {
2524
+ const f = family.trim();
2525
+ if (GENERIC.has(f.toLowerCase()) || f.includes(",") || /^["']/.test(f)) return f;
2526
+ return /^[A-Za-z_-][\w -]*$/.test(f) && !/\s{2}/.test(f) ? f : `"${f.replace(/"/g, '\\"')}"`;
2527
+ }
2528
+ function cssFont(font, family = font.family) {
2529
+ return `${font.italic ? "italic " : ""}${font.weight} ${font.size}px ${cssFontFamily(family)}`;
2530
+ }
2531
+ function createCanvasTextShaper(options = {}) {
2532
+ const make = options.createCanvas ?? ((w, h) => {
2533
+ if (typeof OffscreenCanvas !== "undefined") return new OffscreenCanvas(w, h);
2534
+ if (typeof document !== "undefined") {
2535
+ const c = document.createElement("canvas");
2536
+ c.width = w;
2537
+ c.height = h;
2538
+ return c;
2539
+ }
2540
+ return null;
2541
+ });
2542
+ const canvas = make(1, 1);
2543
+ const ctx = canvas?.getContext("2d");
2544
+ if (!ctx || typeof ctx.measureText !== "function") return null;
2545
+ const fallback = options.fallbackFamily ?? "sans-serif";
2546
+ const available = /* @__PURE__ */ new Map();
2547
+ const widths = /* @__PURE__ */ new Map();
2548
+ const metrics = /* @__PURE__ */ new Map();
2549
+ const probe = "mmmmmmmmmmlli1WQ@#";
2550
+ const width = (font, text) => {
2551
+ ctx.font = font;
2552
+ return ctx.measureText(text).width;
2553
+ };
2554
+ const isAvailable = (family) => {
2555
+ const f = family.trim();
2556
+ if (GENERIC.has(f.toLowerCase()) || f.includes(",")) return true;
2557
+ let hit = available.get(f);
2558
+ if (hit === void 0) {
2559
+ const q = cssFontFamily(f);
2560
+ hit = width(`72px ${q}, monospace`, probe) !== width("72px monospace", probe) || width(`72px ${q}, serif`, probe) !== width("72px serif", probe);
2561
+ available.set(f, hit);
2562
+ }
2563
+ return hit;
2564
+ };
2565
+ return {
2566
+ id: "canvas",
2567
+ resolve(font) {
2568
+ const ok = isAvailable(font.family);
2569
+ return { family: ok ? font.family : fallback, available: ok };
2570
+ },
2571
+ metrics(font) {
2572
+ const key = cssFont(font);
2573
+ let m = metrics.get(key);
2574
+ if (!m) {
2575
+ ctx.font = key;
2576
+ ctx.textBaseline = "alphabetic";
2577
+ const t = ctx.measureText("Hg");
2578
+ const ascent = t.emHeightAscent ?? t.fontBoundingBoxAscent ?? font.size * 0.8;
2579
+ const descent = t.emHeightDescent ?? t.fontBoundingBoxDescent ?? font.size * 0.2;
2580
+ m = { ascent: Number.isFinite(ascent) ? ascent : font.size * 0.8, descent: Number.isFinite(descent) ? descent : font.size * 0.2 };
2581
+ metrics.set(key, m);
2582
+ }
2583
+ return m;
2584
+ },
2585
+ measure(text, font) {
2586
+ const key = `${cssFont(font)}|${text}`;
2587
+ let w = widths.get(key);
2588
+ if (!w) {
2589
+ ctx.font = cssFont(font);
2590
+ w = [0];
2591
+ for (let i = 1; i <= text.length; i++) w.push(ctx.measureText(text.slice(0, i)).width);
2592
+ if (widths.size > 2e3) widths.clear();
2593
+ widths.set(key, w);
2594
+ }
2595
+ return w;
2596
+ },
2597
+ draw(target, text, font, x, y) {
2598
+ target.font = cssFont(font);
2599
+ target.textBaseline = "alphabetic";
2600
+ target.textAlign = "left";
2601
+ target.fillText(text, x, y);
2602
+ },
2603
+ families() {
2604
+ const out = /* @__PURE__ */ new Set(["sans-serif", "serif", "monospace"]);
2605
+ const fonts = typeof document !== "undefined" ? document.fonts : void 0;
2606
+ if (fonts && typeof fonts.forEach === "function") fonts.forEach((f) => out.add(f.family.replace(/^["']|["']$/g, "")));
2607
+ return [...out];
2608
+ }
2609
+ };
2610
+ }
2611
+ function createOutlineTextShaper(fonts, options = {}) {
2612
+ if (!fonts.length) throw new Error("createOutlineTextShaper needs at least one font.");
2613
+ const entries = fonts.map((f) => {
2614
+ const e = "face" in f ? f : { face: f };
2615
+ const names = e.face.names;
2616
+ const family = e.family ?? names?.fontFamily?.en ?? Object.values(names?.fontFamily ?? {})[0] ?? "Font";
2617
+ const weight = e.weight ?? e.face.tables?.os2?.usWeightClass ?? 400;
2618
+ const italic = e.italic ?? Boolean((e.face.tables?.os2?.fsSelection ?? 0) & 1);
2619
+ const postScript = names?.postScriptName?.en ?? Object.values(names?.postScriptName ?? {})[0];
2620
+ return { face: e.face, family, key: family.toLowerCase(), weight, italic, postScript };
2621
+ });
2622
+ const fallbackKey = (options.fallback ?? entries[0].family).toLowerCase();
2623
+ const pick2 = (font) => {
2624
+ if (font.postScriptName) {
2625
+ const exact = entries.find((e) => e.postScript === font.postScriptName);
2626
+ if (exact) return { entry: exact, available: true };
2627
+ }
2628
+ const key = font.family.trim().toLowerCase();
2629
+ let pool = entries.filter((e) => e.key === key);
2630
+ const available = pool.length > 0;
2631
+ if (!available) pool = entries.filter((e) => e.key === fallbackKey);
2632
+ if (!pool.length) pool = entries;
2633
+ const styled = pool.filter((e) => e.italic === font.italic);
2634
+ const list = styled.length ? styled : pool;
2635
+ const entry = list.reduce((best, e) => Math.abs(e.weight - font.weight) < Math.abs(best.weight - font.weight) ? e : best);
2636
+ return { entry, available };
2637
+ };
2638
+ const toPath = (cmds) => {
2639
+ const out = [];
2640
+ for (const c of cmds) {
2641
+ if (c.type === "M" || c.type === "L") out.push({ type: c.type, x: c.x, y: c.y });
2642
+ 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 });
2643
+ else if (c.type === "Q") out.push({ type: "Q", x1: c.x1, y1: c.y1, x: c.x, y: c.y });
2644
+ else out.push({ type: "Z" });
2645
+ }
2646
+ return out;
2647
+ };
2648
+ return {
2649
+ id: options.id ?? `outline:${entries.map((e) => `${e.family}/${e.weight}${e.italic ? "i" : ""}`).join(",")}`,
2650
+ resolve(font) {
2651
+ const { entry, available } = pick2(font);
2652
+ return { family: entry.family, available };
2653
+ },
2654
+ metrics(font) {
2655
+ const { face } = pick2(font).entry;
2656
+ const k = font.size / face.unitsPerEm;
2657
+ return { ascent: face.ascender * k, descent: -face.descender * k };
2658
+ },
2659
+ measure(text, font) {
2660
+ const { face } = pick2(font).entry;
2661
+ const out = [0];
2662
+ for (let i = 1; i <= text.length; i++) out.push(face.getAdvanceWidth(text.slice(0, i), font.size, { kerning: true }));
2663
+ return out;
2664
+ },
2665
+ outline(text, font, x, y) {
2666
+ return toPath(pick2(font).entry.face.getPath(text, x, y, font.size, { kerning: true }).commands);
2667
+ },
2668
+ families() {
2669
+ return [...new Set(entries.map((e) => e.family))];
2670
+ }
2671
+ };
2672
+ }
2673
+
2444
2674
  // src/image-editor/free-transform.ts
2445
2675
  var FREE_TRANSFORM_CONTENTS = ["auto", "layer", "selection", "selection-copy", "selection-outline"];
2446
2676
  var fail = (message) => {
@@ -2512,7 +2742,7 @@ var FreeTransformSession = class {
2512
2742
  }
2513
2743
  const targetId = init.layerId ?? host.activeLayerId();
2514
2744
  const target = targetId && content !== "selection-outline" ? findLayer(host.layers(), targetId)?.layer : void 0;
2515
- const smartTarget = (target?.type === "smart" || target?.type === "shape") && (init.channel ?? (target.id === host.activeLayerId() ? host.activeTarget() : "pixels")) !== "mask";
2745
+ const smartTarget = (target?.type === "smart" || target?.type === "shape" || target?.type === "type") && (init.channel ?? (target.id === host.activeLayerId() ? host.activeTarget() : "pixels")) !== "mask";
2516
2746
  if (content === "auto") content = selected && !smartTarget ? "selection" : "layer";
2517
2747
  this.content = content;
2518
2748
  if (content !== "layer" && !selected) fail(`Nothing is selected; ${content} needs a selection. Select first, or use content layer.`);
@@ -2539,7 +2769,7 @@ var FreeTransformSession = class {
2539
2769
  const linked = mask?.linked !== false;
2540
2770
  const maskAlone = channel === "mask" && (!linked || layer.type !== "pixel");
2541
2771
  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.`);
2542
- 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.`);
2772
+ 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.`);
2543
2773
  if (!layer.visible && content !== "selection-copy") fail(`Layer "${layer.name || id}" is hidden; show it before you transform it.`);
2544
2774
  if (content !== "selection-copy") {
2545
2775
  requireUnlocked(layer, "position", "transform it");
@@ -2561,6 +2791,26 @@ var FreeTransformSession = class {
2561
2791
  this.shown = this.startLayers;
2562
2792
  return;
2563
2793
  }
2794
+ if (layer.type === "type" && !maskAlone) {
2795
+ 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).`);
2796
+ const frame = typeFrame(layer, host.textEnv().shaper);
2797
+ const m = textMatrix(layer);
2798
+ const at = (x, y) => ({ x: m[0] * x + m[2] * y + m[4], y: m[1] * x + m[3] * y + m[5] });
2799
+ this.typeTarget = layer;
2800
+ this.typeLayer = true;
2801
+ this.box = frame;
2802
+ this.startGeometry = {
2803
+ box: frame,
2804
+ 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)],
2805
+ warp: null
2806
+ };
2807
+ this.geometryValue = this.startGeometry;
2808
+ this.startInterp = this.interp;
2809
+ this.moves = mask && linked ? ["pixels", "mask"] : ["pixels"];
2810
+ if (mask && linked) this.parts.push({ what: "mask", offset: o, moving: mask.surface, base: null, reduced: /* @__PURE__ */ new Map() });
2811
+ this.shown = this.startLayers;
2812
+ return;
2813
+ }
2564
2814
  if (layer.type === "shape" && !maskAlone) {
2565
2815
  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).`);
2566
2816
  const g = shapeGeometryBounds(layer);
@@ -2663,6 +2913,9 @@ var FreeTransformSession = class {
2663
2913
  /** True when the layer is a shape layer (the transform maps its paths and renders them again). */
2664
2914
  shapeLayer = false;
2665
2915
  shape = null;
2916
+ /** True when the layer is a type layer (the transform sets its text matrix; affine only). */
2917
+ typeLayer = false;
2918
+ typeTarget = null;
2666
2919
  geometryValue;
2667
2920
  smart = null;
2668
2921
  smartReduced = /* @__PURE__ */ new Map();
@@ -2711,6 +2964,9 @@ var FreeTransformSession = class {
2711
2964
  };
2712
2965
  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.");
2713
2966
  if (!transformMap(next)) fail("These corners fold the frame over itself or through the horizon; keep the quad convex.");
2967
+ if (this.typeTarget && (hasWarp(next) || !matrixFromCorners(next.box, next.corners))) {
2968
+ 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.");
2969
+ }
2714
2970
  this.geometryValue = next;
2715
2971
  if (preview !== false) this.render(preview);
2716
2972
  return next;
@@ -2718,6 +2974,7 @@ var FreeTransformSession = class {
2718
2974
  /** Resamples every part for the current geometry and shows it (or returns it, for commit). */
2719
2975
  resample(final, preview = {}) {
2720
2976
  if (this.smart) return { layers: this.resampleSmart(final, preview), selection: this.startSelection };
2977
+ if (this.typeTarget) return { layers: this.resampleType(), selection: this.startSelection };
2721
2978
  const g = this.geometryValue;
2722
2979
  const identity = isIdentityTransform(g);
2723
2980
  const warp = hasWarp(g);
@@ -2798,6 +3055,27 @@ var FreeTransformSession = class {
2798
3055
  const surface = renderShape(moved, this.host.shapeEnv(), final ? {} : { clip: preview.clip });
2799
3056
  return mapLayer(this.startLayers, layer.id, () => ({ ...moved, surface }));
2800
3057
  }
3058
+ /**
3059
+ * A type layer with its text matrix set from the corners (affine only: scale, rotate, skew and
3060
+ * flips), drawn again; a linked mask follows by the change's homography.
3061
+ */
3062
+ resampleType() {
3063
+ const layer = this.typeTarget;
3064
+ const g = this.geometryValue;
3065
+ if (hasWarp(g)) fail("Type layers cannot be warped here; rasterize the layer first (rasterize_layer), or transform it with scale, rotate and skew.");
3066
+ const m = matrixFromCorners(g.box, g.corners);
3067
+ 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.");
3068
+ const next = { ...layer, transform: m };
3069
+ delete next.annotation;
3070
+ let layers = mapLayer(this.startLayers, layer.id, () => rerenderText(next, this.host.textEnv()));
3071
+ const change = multiplyHomographies(homographyFromAffine(invertMatrix(textMatrix(layer))), homographyFromAffine(m));
3072
+ for (const part of this.parts) {
3073
+ if (part.what !== "mask") continue;
3074
+ const surface = transformSurface(part.moving, change, { interpolation: this.interp === "nearest" ? "nearest" : "bilinear" });
3075
+ layers = mapLayer(layers, layer.id, (l) => ({ ...l, mask: { ...l.mask, surface } }));
3076
+ }
3077
+ return layers;
3078
+ }
2801
3079
  /** A smart object re-rendered from its contents for the current geometry (previews leave the smart filters out). */
2802
3080
  resampleSmart(final, preview) {
2803
3081
  const layer = this.smart;
@@ -2821,7 +3099,7 @@ var FreeTransformSession = class {
2821
3099
  /** Whether the geometry (or the interpolation of a smart object) differs from where it started. */
2822
3100
  isChanged() {
2823
3101
  const g = this.geometryValue;
2824
- if (!this.smart) return !isIdentityTransform(g);
3102
+ if (!this.smart && !this.typeTarget) return !isIdentityTransform(g);
2825
3103
  const a = this.startGeometry;
2826
3104
  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);
2827
3105
  const warp = (w, box) => w && hasWarp({ box, corners: a.corners, warp: w }) ? JSON.stringify(w) : "";
@@ -2858,7 +3136,7 @@ var FreeTransformSession = class {
2858
3136
  const id = this.copyId ?? this.layerId;
2859
3137
  const layer = id ? findLayer(layers, id)?.layer : void 0;
2860
3138
  if (this.moves.includes("pixels") && layer?.type === "pixel") bounds = toDoc(contentBounds(this.host.pixelsOf(layer)), layer.offset);
2861
- else if (this.moves.includes("pixels") && (layer?.type === "smart" || layer?.type === "shape")) bounds = contentBounds(layer.surface);
3139
+ else if (this.moves.includes("pixels") && (layer?.type === "smart" || layer?.type === "shape" || layer?.type === "type")) bounds = contentBounds(layer.surface);
2862
3140
  else if (this.moves.includes("mask") && layer?.mask) bounds = toDoc(contentBounds(layer.mask.surface), layer.type === "pixel" ? layer.offset : { x: 0, y: 0 });
2863
3141
  else if (this.moves.includes("selection")) bounds = selection?.bounds ? { ...selection.bounds } : null;
2864
3142
  return {
@@ -3112,6 +3390,7 @@ var ImageEditorController = class {
3112
3390
  compositor = new LayerCompositor();
3113
3391
  constructor(options = {}) {
3114
3392
  this.maxHistoryDepth = options.maxHistoryDepth ?? 50;
3393
+ if (options.textShaper !== void 0) this.textShaperValue = options.textShaper;
3115
3394
  this.maxHistoryBytes = options.maxHistoryBytes ?? 512 * 1024 * 1024;
3116
3395
  this.onStateChange = options.onStateChange;
3117
3396
  this.renderer = new ImageEditorRenderer();
@@ -3818,6 +4097,21 @@ var ImageEditorController = class {
3818
4097
  const b = shapeGeometryBounds(moved) ?? { x: 0, y: 0 };
3819
4098
  return { offset: { x: Math.round(b.x), y: Math.round(b.y) }, bounds: this.getLayerBounds(id) };
3820
4099
  }
4100
+ if (layer.type === "type") {
4101
+ const m = layer.transform ?? [1, 0, 0, 1, 0, 0];
4102
+ const dx = move.x !== void 0 ? move.x - m[4] : move.dx ?? 0;
4103
+ const dy = move.y !== void 0 ? move.y - m[5] : move.dy ?? 0;
4104
+ if (!Number.isFinite(dx) || !Number.isFinite(dy)) throw new LayerOperationError("x and y must be numbers.");
4105
+ requireUnlocked(layer, "position", "move it");
4106
+ const mx = Math.round(dx);
4107
+ const my = Math.round(dy);
4108
+ let moved = { ...transformTypeLayer(layer, [1, 0, 0, 1, mx, my]), surface: translateSurface(layer.surface, mx, my) };
4109
+ const mask = layer.mask;
4110
+ if (mask && mask.linked !== false && !mask.source && (mx || my)) moved = { ...moved, mask: { ...mask, surface: translateSurface(mask.surface, mx, my) } };
4111
+ this.commitLayers(mapLayer(this.state.layers, id, () => moved), void 0, commitToHistory);
4112
+ const t = moved.transform ?? [1, 0, 0, 1, 0, 0];
4113
+ return { offset: { x: Math.round(t[4]), y: Math.round(t[5]) }, bounds: this.getLayerBounds(id) };
4114
+ }
3821
4115
  const current = layer.type === "pixel" ? layer.offset : { x: 0, y: 0 };
3822
4116
  const x = move.x ?? current.x + (move.dx ?? 0);
3823
4117
  const y = move.y ?? current.y + (move.dy ?? 0);
@@ -3966,6 +4260,7 @@ var ImageEditorController = class {
3966
4260
  canvas: () => this.naturalSize,
3967
4261
  pixelsOf: (layer) => this.surfaceOf(layer),
3968
4262
  shapeEnv: () => this.shapeEnv(),
4263
+ textEnv: () => this.textEnv(),
3969
4264
  capture: () => this.captureHistoryEntry(),
3970
4265
  show: (layers, selection, active) => {
3971
4266
  this.state.layers = layers;
@@ -4066,7 +4361,7 @@ var ImageEditorController = class {
4066
4361
  if (turn !== 90 && turn !== -90 && turn !== 180) throw new LayerOperationError(`turn must be 90, -90 or 180, got ${String(turn)}.`);
4067
4362
  const session = this.beginFreeTransform({ interpolation: "nearest", ...init });
4068
4363
  try {
4069
- const b = session.smartObject ? transformedBox(session.startGeometry) ?? session.box : session.box;
4364
+ const b = session.smartObject || session.typeLayer ? transformedBox(session.startGeometry) ?? session.box : session.box;
4070
4365
  const cx = b.x + b.width / 2;
4071
4366
  const cy = b.y + b.height / 2;
4072
4367
  const ox = turn === 180 ? 0 : Math.round(cx - b.height / 2) - (cx - b.height / 2);
@@ -4786,6 +5081,7 @@ var ImageEditorController = class {
4786
5081
  } else {
4787
5082
  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.`);
4788
5083
  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).`);
5084
+ 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).`);
4789
5085
  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).`);
4790
5086
  requireUnlocked(layer, "pixels", action);
4791
5087
  }
@@ -5491,7 +5787,11 @@ var ImageEditorController = class {
5491
5787
  // --- Annotations & Layers ---
5492
5788
  /** The layer showing annotation `id`: an annotation layer, or a shape layer migrated from it. */
5493
5789
  annotationLayer(id) {
5494
- return flattenLayers(this.state.layers).find((l) => (l.type === "annotation" || l.type === "shape") && annotationOfLayer(l)?.id === id);
5790
+ return flattenLayers(this.state.layers).find((l) => (l.type === "annotation" || l.type === "shape" || l.type === "type") && annotationOfLayer(l)?.id === id);
5791
+ }
5792
+ /** What migrating an annotation needs: the canvas size, patterns and the text shaper. */
5793
+ annotationEnv() {
5794
+ return { ...this.shapeEnv(), ...this.textEnv() };
5495
5795
  }
5496
5796
  /**
5497
5797
  * Adds an annotation as its own layer (top of the stack unless `aboveId` is given). Pen,
@@ -5503,7 +5803,7 @@ var ImageEditorController = class {
5503
5803
  this.recordHistory();
5504
5804
  const id = `ann_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
5505
5805
  const fullAnnotation = { ...annotation, id };
5506
- const layer = annotationLayerFor(fullAnnotation, this.shapeEnv());
5806
+ const layer = annotationLayerFor(fullAnnotation, this.annotationEnv());
5507
5807
  const loc = placement.aboveId ? findLayer(this.state.layers, placement.aboveId) : null;
5508
5808
  this.state.layers = loc ? insertLayer(this.state.layers, layer, loc.parent?.id ?? null, loc.index + 1) : insertLayer(this.state.layers, layer, null);
5509
5809
  this.state.selectedAnnotationId = id;
@@ -5523,24 +5823,14 @@ var ImageEditorController = class {
5523
5823
  }
5524
5824
  this.notify();
5525
5825
  }
5526
- /** `layer` (an annotation or migrated shape layer) showing `annotation` instead, keeping its other properties. */
5826
+ /** `layer` (an annotation, or a shape or type layer migrated from one) showing `annotation` instead, keeping its other properties. */
5527
5827
  layerForAnnotation(layer, annotation, name) {
5528
- const shape = shapeFromAnnotation(annotation);
5529
- const { annotation: _old, ...props } = layer;
5530
- const blend = annotation.blend && annotation.blend !== "normal" ? annotation.blend : "normal";
5531
- if (shape) {
5532
- const base = { ...props, name, blend: layer.type === "annotation" && layer.blend !== "normal" ? layer.blend : blend };
5533
- if (layer.type === "shape") {
5534
- const fresh = rerenderShape({ ...base, components: shape.components, fill: shape.fill, stroke: shape.stroke }, this.shapeEnv());
5535
- delete fresh.fillRule;
5536
- return { ...fresh, annotation };
5537
- }
5538
- const created = createShapeLayer({ ...shape, name, blend: base.blend }, this.shapeEnv(), layer.id);
5539
- const { type: _t2, ...rest2 } = base;
5540
- return { ...rest2, ...created, annotation };
5541
- }
5542
- const { type: _t, components: _c, fill: _f, stroke: _s, surface: _sf, offset: _o, fillRule: _r, fillOpacity: _fo, psd: _p, ...rest } = props;
5543
- return { ...rest, type: "annotation", name, blend: layer.type === "shape" ? "normal" : layer.blend, annotation };
5828
+ const fresh = annotationLayerFor(annotation, this.annotationEnv());
5829
+ const keep = { id: layer.id, visible: layer.visible, opacity: layer.opacity };
5830
+ for (const k of ["clipped", "mask", "locks", "effects"]) if (layer[k] !== void 0) keep[k] = layer[k];
5831
+ if (fresh.type !== "annotation" && layer.type !== "annotation" && layer.fillOpacity !== void 0) keep.fillOpacity = layer.fillOpacity;
5832
+ const blend = fresh.type === "annotation" ? layer.type === "annotation" ? layer.blend : "normal" : layer.type === "annotation" && layer.blend !== "normal" ? layer.blend : fresh.blend;
5833
+ return { ...fresh, ...keep, name, blend };
5544
5834
  }
5545
5835
  removeAnnotation(id) {
5546
5836
  this.recordHistory();
@@ -5555,7 +5845,7 @@ var ImageEditorController = class {
5555
5845
  clearAnnotations() {
5556
5846
  this.recordHistory();
5557
5847
  let layers = this.state.layers;
5558
- for (const l of flattenLayers(layers)) if (l.type === "annotation" || l.type === "shape" && l.annotation) layers = mapLayer(layers, l.id, () => null);
5848
+ for (const l of flattenLayers(layers)) if (l.type === "annotation" || (l.type === "shape" || l.type === "type") && l.annotation) layers = mapLayer(layers, l.id, () => null);
5559
5849
  this.state.layers = layers;
5560
5850
  this.state.selectedAnnotationId = null;
5561
5851
  this.notify();
@@ -5794,6 +6084,233 @@ var ImageEditorController = class {
5794
6084
  this.commitLayers(setLayerMask(this.state.layers, target.id, { surface, defaultValue: 0 }));
5795
6085
  return target.id;
5796
6086
  }
6087
+ // --- Type layers ---
6088
+ textShaperValue;
6089
+ /** The shaper that lays out and draws type layers (`null` when there is none here). */
6090
+ getTextShaper() {
6091
+ if (this.textShaperValue === void 0) this.textShaperValue = createCanvasTextShaper({ createCanvas: (w, h) => this.renderer.createCanvas(w, h) });
6092
+ return this.textShaperValue;
6093
+ }
6094
+ /** Sets the text shaper (fonts) type layers lay out and draw with; existing layers redraw on their next edit. */
6095
+ setTextShaper(shaper) {
6096
+ this.textShaperValue = shaper;
6097
+ this.notify();
6098
+ }
6099
+ /** What type layers lay out and draw with. */
6100
+ textEnv() {
6101
+ return { ...this.naturalSize, shaper: this.getTextShaper(), createCanvas: (w, h) => this.renderer.createCanvas(w, h) };
6102
+ }
6103
+ /** Font families the shaper offers (generic families at least). */
6104
+ listFonts() {
6105
+ return this.getTextShaper()?.families?.() ?? ["sans-serif", "serif", "monospace"];
6106
+ }
6107
+ typeInfo(id) {
6108
+ const layer = requireTypeLayer(this.state.layers, id);
6109
+ const ranges = (runs) => {
6110
+ let at = 0;
6111
+ return runs.map((r) => {
6112
+ const out = { start: at, end: at + r.length, style: JSON.parse(JSON.stringify(r.style)) };
6113
+ at += r.length;
6114
+ return out;
6115
+ });
6116
+ };
6117
+ const shaper = this.getTextShaper();
6118
+ let lines = [];
6119
+ let missing = layer.missingFonts ?? [];
6120
+ if (shaper) {
6121
+ const layout = layoutText(layer, shaper);
6122
+ const r = (n) => Math.round(n * 1e3) / 1e3;
6123
+ lines = layout.lines.map((l) => ({ start: l.start, end: l.end, baseline: r(l.baseline), width: r(l.width) }));
6124
+ if (layer.rendering === "stored") missing = layout.missingFonts;
6125
+ }
6126
+ const info = {
6127
+ id: layer.id,
6128
+ name: layer.name,
6129
+ text: layer.text,
6130
+ runs: ranges(layer.runs),
6131
+ paragraphs: ranges(layer.paragraphs),
6132
+ frame: { ...layer.frame },
6133
+ transform: [...layer.transform ?? [1, 0, 0, 1, 0, 0]],
6134
+ lines,
6135
+ bounds: this.getLayerBounds(id),
6136
+ missingFonts: [...missing],
6137
+ rendering: layer.rendering === "stored" ? "stored" : "live"
6138
+ };
6139
+ if (layer.annotation) info.annotationId = layer.annotation.id;
6140
+ return info;
6141
+ }
6142
+ /** A type layer's text, runs, paragraphs, frame, lines and bounds. */
6143
+ getTextLayer(id) {
6144
+ return this.typeInfo(id);
6145
+ }
6146
+ /**
6147
+ * Adds a type layer (one undo step) and makes it active: point text at `x`, `y`, or paragraph
6148
+ * text wrapped in `box`. Refused without a text shaper. Returns its id.
6149
+ */
6150
+ addTextLayer(init) {
6151
+ const text = normalizeText(init.text);
6152
+ if (!Number.isFinite(init.x) || !Number.isFinite(init.y)) throw new LayerOperationError("x and y must be numbers (document pixels).");
6153
+ const style = patchTextStyle({ ...DEFAULT_TEXT_STYLE, color: this.state.paint.foreground }, init.style ?? {});
6154
+ const paragraph = patchParagraphStyle({}, init.paragraph ?? {});
6155
+ let frame = { kind: "point" };
6156
+ if (init.box) {
6157
+ const { width, height } = init.box;
6158
+ 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.");
6159
+ frame = { kind: "box", width, height };
6160
+ }
6161
+ if (init.rotation !== void 0 && !Number.isFinite(init.rotation)) throw new LayerOperationError("rotation must be a number of degrees.");
6162
+ 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.");
6163
+ if (init.blend !== void 0 && !isBlendMode(init.blend)) throw new LayerOperationError(`Unknown blend mode "${String(init.blend)}".`);
6164
+ const t = [1, 0, 0, 1, init.x, init.y];
6165
+ const transform = init.rotation ? multiplyMatrices(t, rotateMatrix(init.rotation, { x: init.x, y: init.y })) : t;
6166
+ const layer = createTypeLayer({ text, runs: [{ length: text.length, style }], paragraphs: [{ length: text.length, style: paragraph }], frame, transform, name: init.name ?? typeLayerName(text) }, this.textEnv());
6167
+ if (init.opacity !== void 0) layer.opacity = init.opacity;
6168
+ if (init.blend !== void 0) layer.blend = init.blend;
6169
+ const next = placeLayer(this.state.layers, layer, this.newLayerPlacement(init));
6170
+ this.commitLayers(next, () => {
6171
+ this.state.activeLayerId = layer.id;
6172
+ this.state.activeTarget = "pixels";
6173
+ });
6174
+ return layer.id;
6175
+ }
6176
+ /**
6177
+ * Replaces text [start, end) (default all of it) with `text`; inserted text takes the style at
6178
+ * `start`. One undo step unless `commitToHistory` is false (typing). Returns the layer.
6179
+ */
6180
+ setText(id, text, range = {}, commitToHistory = true) {
6181
+ const layer = requireTypeLayer(this.state.layers, id);
6182
+ const insert = normalizeText(text);
6183
+ const [start, end] = textRange(layer.text, range.start, range.end);
6184
+ const { layers } = updateTypeLayer(this.state.layers, id, (l) => replaceTextRange(l, start, end, insert), this.textEnv(), "edit its text");
6185
+ this.commitLayers(layers, void 0, commitToHistory);
6186
+ return this.typeInfo(id);
6187
+ }
6188
+ /** Sets character style fields over [start, end) (default all), one undo step. Returns the layer. */
6189
+ setTextStyle(id, style, range = {}, commitToHistory = true) {
6190
+ const layer = requireTypeLayer(this.state.layers, id);
6191
+ const [start, end] = textRange(layer.text, range.start, range.end);
6192
+ if (!Object.keys(style).length) throw new LayerOperationError("Give at least one style field to set.");
6193
+ patchTextStyle(DEFAULT_TEXT_STYLE, style);
6194
+ const { layers } = updateTypeLayer(
6195
+ this.state.layers,
6196
+ id,
6197
+ (l) => {
6198
+ if (start === end) {
6199
+ if (l.text.length) throw new LayerOperationError("The range is empty; give start < end (or leave both out for the whole text).");
6200
+ return { runs: [{ length: 0, style: patchTextStyle(styleAt(l.runs, 0, DEFAULT_TEXT_STYLE), style) }] };
6201
+ }
6202
+ return { runs: restyleRange(l.runs, start, end, (s) => patchTextStyle(s, style)) };
6203
+ },
6204
+ this.textEnv(),
6205
+ "restyle its text"
6206
+ );
6207
+ this.commitLayers(layers, void 0, commitToHistory);
6208
+ return this.typeInfo(id);
6209
+ }
6210
+ /** Sets paragraph style fields on every paragraph that [start, end) touches (default all), one undo step. */
6211
+ setParagraphStyle(id, style, range = {}, commitToHistory = true) {
6212
+ const layer = requireTypeLayer(this.state.layers, id);
6213
+ const [start, end] = textRange(layer.text, range.start, range.end);
6214
+ if (!Object.keys(style).length) throw new LayerOperationError("Give at least one paragraph field to set.");
6215
+ patchParagraphStyle({}, style);
6216
+ const { layers } = updateTypeLayer(
6217
+ this.state.layers,
6218
+ id,
6219
+ (l) => {
6220
+ let at = 0;
6221
+ const paragraphs = l.paragraphs.map((p) => {
6222
+ const a = at;
6223
+ at += p.length;
6224
+ const touches = a < end && at > start || start === end && start >= a && (start < at || at === l.text.length);
6225
+ return touches || !l.text.length ? { length: p.length, style: patchParagraphStyle(p.style, style) } : p;
6226
+ });
6227
+ return { paragraphs };
6228
+ },
6229
+ this.textEnv(),
6230
+ "restyle its paragraphs"
6231
+ );
6232
+ this.commitLayers(layers, void 0, commitToHistory);
6233
+ return this.typeInfo(id);
6234
+ }
6235
+ /**
6236
+ * Point text ↔ paragraph text: `{ kind: 'box', width, height }` wraps the text in a box (its
6237
+ * top-left where the first line's top-left is now), `{ kind: 'point' }` sets it on one line per
6238
+ * paragraph. Also resizes a box. One undo step.
6239
+ */
6240
+ setTextFrame(id, frame, commitToHistory = true) {
6241
+ const layer = requireTypeLayer(this.state.layers, id);
6242
+ 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.");
6243
+ if (frame.kind !== "box" && frame.kind !== "point") throw new LayerOperationError("kind must be point or box.");
6244
+ const shaper = this.getTextShaper();
6245
+ const { layers } = updateTypeLayer(
6246
+ this.state.layers,
6247
+ id,
6248
+ (l) => {
6249
+ if (frame.kind === "box" && l.frame.kind === "box") return { frame: { kind: "box", width: frame.width, height: frame.height } };
6250
+ if (!shaper) throw new LayerOperationError("Changing between point and paragraph text needs a text shaper to measure the text.");
6251
+ const lines = layoutText(l, shaper).lines;
6252
+ const first = lines[0];
6253
+ const dx = frame.kind === "box" ? first ? first.x : 0 : 0;
6254
+ const dy = frame.kind === "box" ? first ? first.baseline - first.ascent : 0 : first ? first.baseline : 0;
6255
+ const m = textMatrix(l);
6256
+ 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]];
6257
+ const nextFrame = frame.kind === "box" ? { kind: "box", width: frame.width, height: frame.height } : { kind: "point" };
6258
+ return { frame: nextFrame, transform: shifted };
6259
+ },
6260
+ this.textEnv(),
6261
+ "change its frame"
6262
+ );
6263
+ this.commitLayers(layers, void 0, commitToHistory);
6264
+ return this.typeInfo(id);
6265
+ }
6266
+ /**
6267
+ * Convert to Shape: the text's glyph outlines become a shape layer with the same id, name and
6268
+ * layer properties (needs a shaper that gives outlines). The fill is the first run's colour;
6269
+ * `warnings` says when other colours were dropped. One undo step.
6270
+ */
6271
+ convertTextToShape(id) {
6272
+ const layer = requireTypeLayer(this.state.layers, id);
6273
+ requireUnlocked(layer, "all", "convert it");
6274
+ const shaper = this.getTextShaper();
6275
+ if (!shaper) throw new LayerOperationError("Convert to Shape needs a text shaper that gives glyph outlines; none is set here.");
6276
+ const parts = textOutlines(layer, shaper);
6277
+ const subpaths = parts.flatMap((p) => bezierToSubpaths(p.path));
6278
+ if (!subpaths.length) throw new LayerOperationError("The text draws nothing (it is empty or only spaces), so there is no shape to make.");
6279
+ const warnings = [];
6280
+ const colors = new Set(layer.runs.filter((r) => r.length > 0).map((r) => `${r.style.color}/${r.style.opacity ?? 1}`));
6281
+ if (colors.size > 1) warnings.push("The text had several colours; the shape takes the first one.");
6282
+ const missing = layoutText(layer, shaper).missingFonts;
6283
+ if (missing.length) warnings.push(`Missing fonts were drawn with a fallback: ${missing.join(", ")}.`);
6284
+ const first = layer.runs.find((r) => r.length > 0)?.style ?? DEFAULT_TEXT_STYLE;
6285
+ const fill = first.opacity !== void 0 && first.opacity !== 1 ? { type: "color", color: first.color, opacity: first.opacity } : { type: "color", color: first.color };
6286
+ const shape = createShapeLayer({ components: [{ op: "combine", subpaths }], fill, stroke: null, name: layer.name }, this.shapeEnv(), layer.id);
6287
+ const keep = { visible: layer.visible, opacity: layer.opacity, blend: layer.blend };
6288
+ for (const k of ["clipped", "mask", "locks", "effects", "fillOpacity"]) if (layer[k] !== void 0) keep[k] = layer[k];
6289
+ this.commitLayers(mapLayer(this.state.layers, id, () => ({ ...shape, ...keep })));
6290
+ return { layerId: id, warnings };
6291
+ }
6292
+ /** The layout of a type layer (lines, characters, bounds in text space), or `null` without a shaper. */
6293
+ getTextLayout(id) {
6294
+ const layer = requireTypeLayer(this.state.layers, id);
6295
+ const shaper = this.getTextShaper();
6296
+ return shaper ? layoutText(layer, shaper) : null;
6297
+ }
6298
+ /** The text index nearest a document point (for clicks on a type layer). */
6299
+ textIndexAt(id, point) {
6300
+ const layer = requireTypeLayer(this.state.layers, id);
6301
+ const layout = this.getTextLayout(id);
6302
+ return layout ? textIndexAt(layout, toTextSpace(layer, point)) : layer.text.length;
6303
+ }
6304
+ /** The caret at text index `index` as a document-space segment (top to bottom), for drawing it. */
6305
+ textCaret(id, index) {
6306
+ const layer = requireTypeLayer(this.state.layers, id);
6307
+ const layout = this.getTextLayout(id);
6308
+ if (!layout) return null;
6309
+ const c = caretAt(layout, Math.max(0, Math.min(layer.text.length, index)));
6310
+ const m = textMatrix(layer);
6311
+ const at = (x, y) => ({ x: m[0] * x + m[2] * y + m[4], y: m[1] * x + m[3] * y + m[5] });
6312
+ return { top: at(c.x, c.top), bottom: at(c.x, c.bottom), line: c.line };
6313
+ }
5797
6314
  // --- Shape layers ---
5798
6315
  /** What shape rendering needs: the canvas size and the document's patterns. */
5799
6316
  shapeEnv() {
@@ -5970,11 +6487,11 @@ var ImageEditorController = class {
5970
6487
  }
5971
6488
  } else if (layer.type === "smart") {
5972
6489
  return this.rasterizeSmartObject(id);
5973
- } else if (layer.type === "shape") {
5974
- next = shapeAsPixelLayer(layer);
6490
+ } else if (layer.type === "shape" || layer.type === "type") {
6491
+ next = layer.type === "shape" ? shapeAsPixelLayer(layer) : typeAsPixelLayer(layer);
5975
6492
  if (this.state.selectedAnnotationId && this.state.selectedAnnotationId === layer.annotation?.id) this.state.selectedAnnotationId = null;
5976
6493
  } else {
5977
- 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.`);
6494
+ 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.`);
5978
6495
  }
5979
6496
  this.commitLayers(mapLayer(this.state.layers, id, () => next));
5980
6497
  return id;
@@ -5997,7 +6514,7 @@ var ImageEditorController = class {
5997
6514
  s.sourceUrl = null;
5998
6515
  s.imageDimensions = { width: result.width, height: result.height };
5999
6516
  if (!this.samplePixelsOverride) this.samplePixels = null;
6000
- s.layers = result.layers;
6517
+ s.layers = this.checkPsdFonts(result);
6001
6518
  s.crop = null;
6002
6519
  s.transform = { ...DEFAULT_TRANSFORM_VALUES };
6003
6520
  s.outputOpacity = 1;
@@ -6022,6 +6539,30 @@ var ImageEditorController = class {
6022
6539
  summary: result.summary
6023
6540
  };
6024
6541
  }
6542
+ /**
6543
+ * Type layers whose fonts the shaper lacks: they keep Photoshop's pixels, are marked with their
6544
+ * `missingFonts`, and a warning names them (an edit draws them with a fallback).
6545
+ */
6546
+ checkPsdFonts(result) {
6547
+ const shaper = this.getTextShaper();
6548
+ const types = flattenLayers(result.layers).filter((l) => l.type === "type");
6549
+ if (!types.length) return result.layers;
6550
+ let layers = result.layers;
6551
+ const missing = /* @__PURE__ */ new Map();
6552
+ for (const l of types) {
6553
+ const fonts = shaper ? layoutText(l, shaper).missingFonts : [...new Set(l.runs.map((r) => r.style.fontFamily))];
6554
+ if (!fonts.length) continue;
6555
+ layers = mapLayer(layers, l.id, () => ({ ...l, missingFonts: fonts }));
6556
+ for (const f of fonts) missing.set(f, [...missing.get(f) ?? [], l.name]);
6557
+ }
6558
+ for (const [font, names] of missing) {
6559
+ result.warnings.push(
6560
+ shaper ? `The font "${font}" is missing here: the type shows Photoshop's pixels until it is edited, then a fallback font (${names.length === 1 ? "layer" : "layers"} ${names.map((n) => `"${n}"`).join(", ")}).` : `Type layers show Photoshop's pixels; nothing here can draw text, so they cannot be edited (${names.length === 1 ? "layer" : "layers"} ${names.map((n) => `"${n}"`).join(", ")}).`
6561
+ );
6562
+ if (!shaper) break;
6563
+ }
6564
+ return layers;
6565
+ }
6025
6566
  /**
6026
6567
  * The document as an 8-bit RGB PSD (PSB above 30 000 px) with real layers, groups, adjustment
6027
6568
  * layers and masks, in the source frame. Layer filters and parametric (depth) masks are baked
@@ -6099,9 +6640,9 @@ var ImageEditorController = class {
6099
6640
  if (mask2) out2.mask = mask2;
6100
6641
  return out2;
6101
6642
  }
6102
- if (layer.type === "shape") {
6103
- add2("Shape layers were written as pixel layers", layer.name);
6104
- const out2 = shapeAsPixelLayer(layer);
6643
+ if (layer.type === "shape" || layer.type === "type") {
6644
+ if (layer.type === "type" && layer.missingFonts?.length) add2(`Type layers drawn with a fallback for missing fonts (${layer.missingFonts.join(", ")}) keep the font names, so Photoshop may draw them differently`, layer.name);
6645
+ const out2 = { ...layer };
6105
6646
  const mask2 = bakeMask(layer);
6106
6647
  if (mask2) out2.mask = mask2;
6107
6648
  else delete out2.mask;
@@ -6203,7 +6744,7 @@ var ImageEditorController = class {
6203
6744
  const bg = developTarget(layers, null);
6204
6745
  if (bg && bg.source !== "document") layers = mapLayer(layers, bg.id, () => ({ ...bg, source: "document" }));
6205
6746
  if (clearAnnotations) {
6206
- for (const l of flattenLayers(layers)) if (l.type === "annotation" || l.type === "shape" && l.annotation) layers = mapLayer(layers, l.id, () => null);
6747
+ for (const l of flattenLayers(layers)) if (l.type === "annotation" || (l.type === "shape" || l.type === "type") && l.annotation) layers = mapLayer(layers, l.id, () => null);
6207
6748
  this.state.selectedAnnotationId = null;
6208
6749
  }
6209
6750
  this.state.layers = layers;
@@ -6279,7 +6820,8 @@ var ImageEditorController = class {
6279
6820
  problems,
6280
6821
  smartContents: Array.isArray(data.smartContents) ? data.smartContents : [],
6281
6822
  canvas,
6282
- pattern: (pid) => storedPatterns.find((p) => p.id === pid) ?? null
6823
+ pattern: (pid) => storedPatterns.find((p) => p.id === pid) ?? null,
6824
+ text: this.textEnv()
6283
6825
  });
6284
6826
  this.lastLoadProblems = problems;
6285
6827
  s.crop = data.crop ? { ...data.crop } : null;
@@ -6482,7 +7024,7 @@ function collectStateTiles(layers, into) {
6482
7024
  for (const l of flattenLayers(layers)) {
6483
7025
  if (l.type === "pixel" && l.source !== "document") collectTiles(l.surface, into);
6484
7026
  if (l.mask && !l.mask.source) collectTiles(l.mask.surface, into);
6485
- if (l.type === "shape") collectTiles(l.surface, into);
7027
+ if (l.type === "shape" || l.type === "type") collectTiles(l.surface, into);
6486
7028
  if (l.type === "smart") {
6487
7029
  collectTiles(l.surface, into);
6488
7030
  if (l.smartFilters?.mask) collectTiles(l.smartFilters.mask.surface, into);
@@ -6531,7 +7073,7 @@ function resizePixels(px2, width, height) {
6531
7073
  function withoutAnnotations(layers) {
6532
7074
  const out = [];
6533
7075
  for (const l of layers) {
6534
- if (l.type === "annotation" || l.type === "shape" && l.annotation) continue;
7076
+ if (l.type === "annotation" || (l.type === "shape" || l.type === "type") && l.annotation) continue;
6535
7077
  out.push(l.type === "group" ? { ...l, children: withoutAnnotations(l.children) } : l);
6536
7078
  }
6537
7079
  return out;
@@ -6721,7 +7263,7 @@ function loadImageEditorDocument(input) {
6721
7263
  if (state.selection === void 0) delete state.selection;
6722
7264
  return { state, report: result.report, unknown: result.unknown };
6723
7265
  }
6724
- var LAYER_TYPES = /* @__PURE__ */ new Set(["pixel", "adjustment", "group", "annotation", "smart", "shape"]);
7266
+ var LAYER_TYPES = /* @__PURE__ */ new Set(["pixel", "adjustment", "group", "annotation", "smart", "shape", "type"]);
6725
7267
  function cleanLayers(value, dropped) {
6726
7268
  const out = [];
6727
7269
  for (const raw of value) {
@@ -7781,6 +8323,18 @@ function describeLayer(layer, activeId, boundsOf) {
7781
8323
  };
7782
8324
  if (layer.annotation) out.annotation = { id: layer.annotation.id, type: layer.annotation.type };
7783
8325
  break;
8326
+ case "type":
8327
+ out.bounds = boundsOf(layer);
8328
+ out.fillOpacity = round3(layer.fillOpacity ?? 1);
8329
+ out.text = {
8330
+ text: layer.text.length > 80 ? `${layer.text.slice(0, 80)}\u2026` : layer.text,
8331
+ frame: layer.frame.kind,
8332
+ runs: layer.runs.length,
8333
+ rendering: layer.rendering === "stored" ? "stored" : "live",
8334
+ missingFonts: layer.missingFonts ?? []
8335
+ };
8336
+ if (layer.annotation) out.annotation = { id: layer.annotation.id, type: layer.annotation.type };
8337
+ break;
7784
8338
  case "smart":
7785
8339
  out.bounds = boundsOf(layer);
7786
8340
  out.fillOpacity = round3(layer.fillOpacity ?? 1);
@@ -7826,7 +8380,7 @@ function createImageLayerTools(controller) {
7826
8380
  const listLayers = {
7827
8381
  name: "list_layers",
7828
8382
  title: "List image layers",
7829
- 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.",
8383
+ 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.",
7830
8384
  inputSchema: { type: "object", properties: {}, additionalProperties: false },
7831
8385
  annotations: { title: "List image layers", ...READ2 },
7832
8386
  handler: () => {
@@ -8170,7 +8724,7 @@ function createImageLayerTools(controller) {
8170
8724
  const rasterize = {
8171
8725
  name: "rasterize_layer",
8172
8726
  title: "Rasterize a layer",
8173
- 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.",
8727
+ 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.",
8174
8728
  inputSchema: { type: "object", properties: { layerId: LAYER_ID2 }, required: ["layerId"], additionalProperties: false },
8175
8729
  annotations: { title: "Rasterize a layer", ...DESTRUCTIVE },
8176
8730
  handler: async ({ layerId }) => ({ layer: describe(await controller.rasterizeLayer(exists(layerId))) })
@@ -8501,7 +9055,7 @@ function createImageEffectTools(controller) {
8501
9055
  const add2 = {
8502
9056
  name: "add_layer_effect",
8503
9057
  title: "Add a layer effect",
8504
- description: "Adds a live effect to a pixel, annotation or group layer with Photoshop's defaults, changed by settings: drop-shadow, inner-shadow, outer-glow, inner-glow, bevel-emboss, satin, color-overlay, gradient-overlay, pattern-overlay or stroke. Several of one kind are allowed (the first listed is on top). One undo step. Returns the new effect's id and the layer's effects.",
9058
+ description: "Adds a live effect to a pixel, shape, type, smart object, annotation or group layer with Photoshop's defaults, changed by settings: drop-shadow, inner-shadow, outer-glow, inner-glow, bevel-emboss, satin, color-overlay, gradient-overlay, pattern-overlay or stroke. Several of one kind are allowed (the first listed is on top). One undo step. Returns the new effect's id and the layer's effects.",
8505
9059
  inputSchema: {
8506
9060
  type: "object",
8507
9061
  $defs: DEFS,
@@ -8975,7 +9529,7 @@ function createImagePsdTools(controller) {
8975
9529
  const open = {
8976
9530
  name: "import_psd",
8977
9531
  title: "Open a PSD as the document",
8978
- description: "Opens a PSD / PSB as the document, replacing the current layers, crop and rotation (one undo step; undo brings the previous document back). Pixel, group and adjustment layers, masks, blend modes, opacity, fill, clipping, visibility, locks, layer effects (with the patterns and global light of the file) map directly; smart objects come in as smart objects (embedded contents, placement and warp, smart filters: Box Blur is drawn, other Photoshop filters are kept but not drawn, filter mask; linked contents as their stored pixels); type, fill and shape layers come in as their pixels; unsupported adjustments are left out. Returns the new layer tree with ids, the size and the warnings (each names the affected layers).",
9532
+ description: "Opens a PSD / PSB as the document, replacing the current layers, crop and rotation (one undo step; undo brings the previous document back). Pixel, group and adjustment layers, masks, blend modes, opacity, fill, clipping, visibility, locks, layer effects (with the patterns and global light of the file) map directly; smart objects come in as smart objects (embedded contents, placement and warp, smart filters: Box Blur is drawn, other Photoshop filters are kept but not drawn, filter mask; linked contents as their stored pixels); shape layers come in as live shapes (paths, path operations, fill, stroke, live rectangles, ellipses and lines); type layers come in as live text (runs, paragraphs, frame, transform) showing Photoshop\u2019s pixels until edited, with a warning for fonts missing here; fill layers come in as their pixels; unsupported adjustments are left out. Returns the new layer tree with ids, the size and the warnings (each names the affected layers).",
8979
9533
  inputSchema: { type: "object", properties: { data: DATA }, required: ["data"], additionalProperties: false },
8980
9534
  annotations: { title: "Open a PSD as the document", ...DESTRUCTIVE4 },
8981
9535
  handler: ({ data }) => {
@@ -8994,7 +9548,7 @@ function createImagePsdTools(controller) {
8994
9548
  const save = {
8995
9549
  name: "export_psd",
8996
9550
  title: "Export the document as a PSD",
8997
- description: "Writes the document as an 8-bit RGB PSD (a PSB above 30000 px) with real layers, groups, adjustment layers, masks and layer effects (an effects block opened from a PSD is written back unchanged while its effects are; the patterns effects use are embedded), in the source frame (crop and rotation are not applied). Smart objects are written as Photoshop smart objects (contents embedded once per linked set, placement, warp, smart filters and filter mask; a Develop smart filter has no Photoshop filter and is left out of the list). Layer filters and depth masks are baked, annotation layers are rasterized (in a browser; elsewhere left out), and the merged image is the editor's composite. Changes nothing. Returns the file as base64, its size in bytes, whether it is a PSB, and warnings for everything that was changed or left out.",
9551
+ description: "Writes the document as an 8-bit RGB PSD (a PSB above 30000 px) with real layers, groups, adjustment layers, masks and layer effects (an effects block opened from a PSD is written back unchanged while its effects are; the patterns effects use are embedded), in the source frame (crop and rotation are not applied). Smart objects are written as Photoshop smart objects (contents embedded once per linked set, placement, warp, smart filters and filter mask; a Develop smart filter has no Photoshop filter and is left out of the list). Shape layers are written as Photoshop shape layers (path, fill, stroke, live shapes) and type layers as Photoshop type layers (text, runs, paragraphs, frame; blocks opened from a PSD are written back unchanged while the layer is). Layer filters and depth masks are baked, annotation layers are rasterized (in a browser; elsewhere left out), and the merged image is the editor's composite. Changes nothing. Returns the file as base64, its size in bytes, whether it is a PSB, and warnings for everything that was changed or left out.",
8998
9552
  inputSchema: { type: "object", properties: {}, additionalProperties: false },
8999
9553
  annotations: { title: "Export the document as a PSD", ...READ5 },
9000
9554
  handler: async () => {
@@ -10019,6 +10573,176 @@ function createImageShapeTools(controller) {
10019
10573
  return [get, add2, update, setComponent, addComponent, removeComponent, merge];
10020
10574
  }
10021
10575
 
10576
+ // src/image-editor/type-tools.ts
10577
+ var READ9 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
10578
+ var WRITE8 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
10579
+ var WRITE_IDEMPOTENT6 = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
10580
+ var DESTRUCTIVE7 = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
10581
+ var LAYER_ID6 = { type: "string", description: "Type layer id from list_layers (type type) or add_text_layer." };
10582
+ var START = { type: "integer", minimum: 0, description: "Start of the range in UTF-16 code units, as get_text_layer counts them (default 0)." };
10583
+ var END = { type: "integer", minimum: 0, description: "End of the range, exclusive (default the end of the text)." };
10584
+ var STYLE = {
10585
+ fontFamily: { type: "string", description: "Font family name (list_fonts shows what is available; a missing family falls back, with a warning)." },
10586
+ fontWeight: { type: "integer", minimum: 1, maximum: 1e3, description: "Weight 100..900 (400 regular, 700 bold)." },
10587
+ italic: { type: "boolean", description: "Italic." },
10588
+ fontSize: { type: "number", minimum: 0.5, maximum: 5e3, description: "Size in document pixels." },
10589
+ color: { type: "string", description: "Colour (#rrggbb, or a CSS colour; its alpha becomes opacity)." },
10590
+ opacity: { type: ["number", "null"], minimum: 0, maximum: 1, description: "Text opacity 0..1 (null resets to 1)." },
10591
+ tracking: { type: ["number", "null"], minimum: -1e3, maximum: 1e4, description: "Letter spacing in 1/1000 em (null resets to 0)." },
10592
+ leading: { type: ["number", "null"], minimum: 0, maximum: 1e4, description: "Baseline-to-baseline distance in pixels (null = auto, the paragraph autoLeading \xD7 size)." },
10593
+ baselineShift: { type: ["number", "null"], minimum: -5e3, maximum: 5e3, description: "Pixels; positive raises the text (null resets to 0)." },
10594
+ underline: { type: "boolean", description: "Underline." },
10595
+ strikethrough: { type: "boolean", description: "Strike through." }
10596
+ };
10597
+ var PARAGRAPH = {
10598
+ align: { type: "string", enum: ["left", "center", "right", "justify"], description: "Alignment (justify stretches paragraph-box lines but the last)." },
10599
+ autoLeading: { type: ["number", "null"], minimum: 0.1, maximum: 10, description: "Auto leading as a multiple of the font size (default 1.2)." },
10600
+ firstLineIndent: { type: ["number", "null"], description: "First-line indent in pixels." },
10601
+ startIndent: { type: ["number", "null"], description: "Left indent in pixels." },
10602
+ endIndent: { type: ["number", "null"], description: "Right indent in pixels." },
10603
+ spaceBefore: { type: ["number", "null"], description: "Space above the paragraph in pixels (not before the first)." },
10604
+ spaceAfter: { type: ["number", "null"], description: "Space below the paragraph in pixels." }
10605
+ };
10606
+ function pick(args, keys) {
10607
+ const out = {};
10608
+ for (const k of keys) if (args[k] !== void 0) out[k] = args[k];
10609
+ return out;
10610
+ }
10611
+ function createImageTypeTools(controller) {
10612
+ const typeId = (id) => {
10613
+ if (typeof id !== "string" || !id) throw new LayerOperationError("layerId is required (a type layer id from list_layers).");
10614
+ requireTypeLayer(controller.getLayers(), id);
10615
+ return id;
10616
+ };
10617
+ const layerSummary = (id) => {
10618
+ const s = controller.getState();
10619
+ const loc = findLayer(s.layers, id);
10620
+ return loc ? { ...describeLayer(loc.layer, s.activeLayerId, (x) => controller.getLayerBounds(x.id)), parentId: loc.parent?.id ?? null, index: loc.index } : null;
10621
+ };
10622
+ const range = (args) => ({ start: args.start, end: args.end });
10623
+ const listFonts = {
10624
+ name: "list_fonts",
10625
+ title: "List fonts",
10626
+ description: "Returns the font families type layers can use here (generic families such as sans-serif always work). Changes nothing.",
10627
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
10628
+ annotations: { title: "List fonts", ...READ9 },
10629
+ handler: () => ({ families: controller.listFonts(), shaper: controller.getTextShaper()?.id ?? null })
10630
+ };
10631
+ const get = {
10632
+ name: "get_text_layer",
10633
+ title: "Read a type layer",
10634
+ 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.",
10635
+ inputSchema: { type: "object", properties: { layerId: LAYER_ID6 }, required: ["layerId"], additionalProperties: false },
10636
+ annotations: { title: "Read a type layer", ...READ9 },
10637
+ handler: ({ layerId }) => controller.getTextLayer(typeId(layerId))
10638
+ };
10639
+ const add2 = {
10640
+ name: "add_text_layer",
10641
+ title: "Add a type layer",
10642
+ 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.",
10643
+ inputSchema: {
10644
+ type: "object",
10645
+ properties: {
10646
+ text: { type: "string", description: "The text; \\n starts a new paragraph." },
10647
+ x: { type: "number", description: "Document x of the origin, pixels." },
10648
+ y: { type: "number", description: "Document y of the origin (point text: the first baseline), pixels." },
10649
+ box: {
10650
+ type: "object",
10651
+ description: "Make it paragraph (area) text wrapped in this box, pixels.",
10652
+ properties: { width: { type: "number", minimum: 1, description: "Box width, pixels." }, height: { type: "number", minimum: 1, description: "Box height, pixels." } },
10653
+ required: ["width", "height"],
10654
+ additionalProperties: false
10655
+ },
10656
+ style: { type: "object", description: "Character style.", properties: STYLE, additionalProperties: false },
10657
+ paragraph: { type: "object", description: "Paragraph style.", properties: PARAGRAPH, additionalProperties: false },
10658
+ rotation: { type: "number", description: "Degrees clockwise about the origin (default 0)." },
10659
+ name: { type: "string", description: "Layer name (default the start of the text)." },
10660
+ opacity: { type: "number", minimum: 0, maximum: 1, description: "Layer opacity 0..1 (default 1)." },
10661
+ blend: { type: "string", enum: BLEND_MODES.map((m) => m.id), description: "Blend mode (default normal)." },
10662
+ aboveId: { type: "string", description: "Put the layer directly above this layer (default above the active layer)." },
10663
+ belowId: { type: "string", description: "Put the layer directly below this layer." },
10664
+ parentId: { type: "string", description: "Put the layer inside this group." },
10665
+ index: { type: "integer", minimum: 0, description: "Position among the siblings, 0 = bottom." }
10666
+ },
10667
+ required: ["text", "x", "y"],
10668
+ additionalProperties: false
10669
+ },
10670
+ annotations: { title: "Add a type layer", ...WRITE8 },
10671
+ handler: (args) => {
10672
+ const init = pick(args, ["text", "x", "y", "box", "style", "paragraph", "rotation", "name", "opacity", "blend", "aboveId", "belowId", "parentId", "index"]);
10673
+ if (init.blend !== void 0) init.blend = init.blend;
10674
+ const id = controller.addTextLayer(init);
10675
+ return { layer: layerSummary(id), text: controller.getTextLayer(id) };
10676
+ }
10677
+ };
10678
+ const setText = {
10679
+ name: "set_text",
10680
+ title: "Edit text",
10681
+ 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.",
10682
+ 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 },
10683
+ annotations: { title: "Edit text", ...WRITE8 },
10684
+ handler: (args) => controller.setText(typeId(args.layerId), args.text, range(args))
10685
+ };
10686
+ const setStyle = {
10687
+ name: "set_text_style",
10688
+ title: "Style a range of text",
10689
+ 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.",
10690
+ inputSchema: { type: "object", properties: { layerId: LAYER_ID6, start: START, end: END, ...STYLE }, required: ["layerId"], additionalProperties: false },
10691
+ annotations: { title: "Style a range of text", ...WRITE_IDEMPOTENT6 },
10692
+ handler: (args) => {
10693
+ const style = pick(args, Object.keys(STYLE));
10694
+ if (!Object.keys(style).length) throw new LayerOperationError(`Give at least one style field: ${Object.keys(STYLE).join(", ")}.`);
10695
+ return controller.setTextStyle(typeId(args.layerId), style, range(args));
10696
+ }
10697
+ };
10698
+ const setParagraph = {
10699
+ name: "set_paragraph_style",
10700
+ title: "Style paragraphs",
10701
+ description: "Sets paragraph fields (alignment, auto leading, indents, spacing) on every paragraph that [start, end) touches (default all). Returns the text layer.",
10702
+ inputSchema: { type: "object", properties: { layerId: LAYER_ID6, start: START, end: END, ...PARAGRAPH }, required: ["layerId"], additionalProperties: false },
10703
+ annotations: { title: "Style paragraphs", ...WRITE_IDEMPOTENT6 },
10704
+ handler: (args) => {
10705
+ const style = pick(args, Object.keys(PARAGRAPH));
10706
+ if (!Object.keys(style).length) throw new LayerOperationError(`Give at least one paragraph field: ${Object.keys(PARAGRAPH).join(", ")}.`);
10707
+ return controller.setParagraphStyle(typeId(args.layerId), style, range(args));
10708
+ }
10709
+ };
10710
+ const setFrame = {
10711
+ name: "set_text_frame",
10712
+ title: "Point or paragraph text",
10713
+ 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.",
10714
+ inputSchema: {
10715
+ type: "object",
10716
+ properties: {
10717
+ layerId: LAYER_ID6,
10718
+ kind: { type: "string", enum: ["point", "box"], description: "point or box." },
10719
+ width: { type: "number", minimum: 1, description: "Box width in pixels (kind box)." },
10720
+ height: { type: "number", minimum: 1, description: "Box height in pixels (kind box)." }
10721
+ },
10722
+ required: ["layerId", "kind"],
10723
+ additionalProperties: false
10724
+ },
10725
+ annotations: { title: "Point or paragraph text", ...WRITE_IDEMPOTENT6 },
10726
+ handler: ({ layerId, kind, width, height }) => {
10727
+ const id = typeId(layerId);
10728
+ if (kind === "box" && (width === void 0 || height === void 0)) throw new LayerOperationError("kind box needs width and height in pixels.");
10729
+ return controller.setTextFrame(id, kind === "box" ? { kind: "box", width, height } : { kind: "point" });
10730
+ }
10731
+ };
10732
+ const toShape = {
10733
+ name: "convert_text_to_shape",
10734
+ title: "Convert text to a shape",
10735
+ 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.",
10736
+ inputSchema: { type: "object", properties: { layerId: LAYER_ID6 }, required: ["layerId"], additionalProperties: false },
10737
+ annotations: { title: "Convert text to a shape", ...DESTRUCTIVE7 },
10738
+ handler: ({ layerId }) => {
10739
+ const { layerId: id, warnings } = controller.convertTextToShape(typeId(layerId));
10740
+ return { layer: layerSummary(id), shape: controller.getShapeLayer(id), warnings };
10741
+ }
10742
+ };
10743
+ return [listFonts, get, add2, setText, setStyle, setParagraph, setFrame, toShape];
10744
+ }
10745
+
10022
10746
  // src/image-editor/tools.ts
10023
10747
  var LAYER_PATH = /^layers\.([^.]+)\.(.+)$/;
10024
10748
  function adjustmentsSummary(adjustments) {
@@ -10259,6 +10983,7 @@ function createImageEditorToolProvider(controller, options = {}) {
10259
10983
  ...createImageTransformTools(controller),
10260
10984
  ...createImageSmartObjectTools(controller),
10261
10985
  ...createImageShapeTools(controller),
10986
+ ...createImageTypeTools(controller),
10262
10987
  ...createImageEffectTools(controller),
10263
10988
  ...createImagePaintTools(controller),
10264
10989
  ...createPigmentTools(),
@@ -10561,6 +11286,470 @@ function applyRegionBlur(imageData, regions, radius = 10) {
10561
11286
  return imageData;
10562
11287
  }
10563
11288
 
11289
+ // src/image-editor/layers/shape-type-ui.ts
11290
+ var DEFAULT_SHAPE_TOOL_OPTIONS = {
11291
+ kind: "rect",
11292
+ fill: "#4f46e5",
11293
+ stroke: null,
11294
+ strokeWidth: 2,
11295
+ radius: 0,
11296
+ sides: 6,
11297
+ points: 5,
11298
+ innerRatio: 0.5,
11299
+ weight: 4,
11300
+ arrow: false
11301
+ };
11302
+ var SHAPE_KIND_LABELS = { rect: "Rectangle", ellipse: "Ellipse", polygon: "Polygon", star: "Star", line: "Line" };
11303
+ var DEFAULT_TYPE_TOOL_OPTIONS = { fontFamily: "sans-serif", fontSize: 32, fontWeight: 400, italic: false, color: "#111827", align: "left" };
11304
+ function liveShapeForDrag(o, a, b, mods = {}) {
11305
+ if (o.kind === "line") {
11306
+ let x2 = b.x;
11307
+ let y2 = b.y;
11308
+ if (mods.shift) {
11309
+ const ang = Math.round(Math.atan2(b.y - a.y, b.x - a.x) / (Math.PI / 4)) * (Math.PI / 4);
11310
+ const len = Math.hypot(b.x - a.x, b.y - a.y);
11311
+ x2 = a.x + Math.cos(ang) * len;
11312
+ y2 = a.y + Math.sin(ang) * len;
11313
+ }
11314
+ if (Math.hypot(x2 - a.x, y2 - a.y) < 1) return null;
11315
+ const head = o.arrow ? { endArrow: { width: o.weight * 4, length: o.weight * 4 } } : {};
11316
+ return { kind: "line", x1: a.x, y1: a.y, x2, y2, weight: o.weight, ...head };
11317
+ }
11318
+ let w = b.x - a.x;
11319
+ let h = b.y - a.y;
11320
+ if (mods.shift) {
11321
+ const s = Math.max(Math.abs(w), Math.abs(h));
11322
+ w = Math.sign(w || 1) * s;
11323
+ h = Math.sign(h || 1) * s;
11324
+ }
11325
+ let x = a.x;
11326
+ let y = a.y;
11327
+ if (mods.alt) {
11328
+ x = a.x - w;
11329
+ y = a.y - h;
11330
+ w *= 2;
11331
+ h *= 2;
11332
+ }
11333
+ const box = { x: Math.min(x, x + w), y: Math.min(y, y + h), width: Math.abs(w), height: Math.abs(h) };
11334
+ if (box.width < 1 || box.height < 1) return null;
11335
+ switch (o.kind) {
11336
+ case "rect":
11337
+ return { kind: "rect", ...box, ...o.radius > 0 ? { radius: o.radius } : {} };
11338
+ case "ellipse":
11339
+ return { kind: "ellipse", ...box };
11340
+ case "polygon":
11341
+ return { kind: "polygon", ...box, sides: o.sides };
11342
+ case "star":
11343
+ return { kind: "star", ...box, points: o.points, innerRatio: o.innerRatio };
11344
+ }
11345
+ return null;
11346
+ }
11347
+ var ShapeDrawGesture = class {
11348
+ constructor(controller, options) {
11349
+ this.controller = controller;
11350
+ this.options = options;
11351
+ }
11352
+ controller;
11353
+ options;
11354
+ start = null;
11355
+ layerId = null;
11356
+ error = null;
11357
+ get active() {
11358
+ return this.start !== null;
11359
+ }
11360
+ /** The shape layer drawn by the last drag (until the next one starts). */
11361
+ get drawnLayerId() {
11362
+ return this.layerId;
11363
+ }
11364
+ down(p) {
11365
+ this.error = null;
11366
+ this.start = { x: p.x, y: p.y };
11367
+ this.layerId = null;
11368
+ }
11369
+ move(p, mods = {}) {
11370
+ if (!this.start) return;
11371
+ const o = this.options();
11372
+ const shape = liveShapeForDrag(o, this.start, p, mods);
11373
+ if (!shape) return;
11374
+ try {
11375
+ if (!this.layerId) {
11376
+ const isLine = o.kind === "line";
11377
+ this.layerId = this.controller.addShapeLayer({
11378
+ shape,
11379
+ fill: isLine ? o.stroke ?? o.fill ?? "#000000" : o.fill,
11380
+ stroke: !isLine && o.stroke ? { width: o.strokeWidth, paint: o.stroke } : null
11381
+ });
11382
+ } else this.controller.setShapeComponent(this.layerId, 0, { shape }, false);
11383
+ } catch (e) {
11384
+ this.error = e instanceof Error ? e.message : String(e);
11385
+ this.start = null;
11386
+ }
11387
+ }
11388
+ up() {
11389
+ this.start = null;
11390
+ return this.layerId;
11391
+ }
11392
+ cancel() {
11393
+ this.start = null;
11394
+ }
11395
+ };
11396
+ function applyShapeOptions(controller, layerId, change) {
11397
+ const info = controller.getShapeLayer(layerId);
11398
+ const style = {};
11399
+ if ("fill" in change) style.fill = change.fill ?? null;
11400
+ if ("stroke" in change || "strokeWidth" in change) {
11401
+ const color = "stroke" in change ? change.stroke : info.stroke?.paint.type === "color" ? info.stroke.paint.color : null;
11402
+ style.stroke = color ? { width: change.strokeWidth ?? info.stroke?.width ?? 2, paint: color } : null;
11403
+ }
11404
+ if (Object.keys(style).length) controller.setShapeStyle(layerId, style);
11405
+ const live = info.components[0]?.live;
11406
+ if (!live) return;
11407
+ const shape = {};
11408
+ if (live.kind === "rect" && change.radius !== void 0) shape.radius = change.radius;
11409
+ if (live.kind === "polygon" && change.sides !== void 0) shape.sides = change.sides;
11410
+ if (live.kind === "star" && change.points !== void 0) shape.points = change.points;
11411
+ if (live.kind === "star" && change.innerRatio !== void 0) shape.innerRatio = change.innerRatio;
11412
+ if (live.kind === "line" && change.weight !== void 0) shape.weight = change.weight;
11413
+ if (live.kind === "line" && change.arrow !== void 0) shape.endArrow = change.arrow ? { width: (live.weight || 1) * 4, length: (live.weight || 1) * 4 } : null;
11414
+ if (Object.keys(shape).length) controller.setShapeComponent(layerId, 0, { shape });
11415
+ }
11416
+ var TextEditSession = class {
11417
+ constructor(controller, layerId, index) {
11418
+ this.controller = controller;
11419
+ this.layerId = layerId;
11420
+ const n = this.text.length;
11421
+ this.anchor = this.focus = Math.max(0, Math.min(n, index ?? n));
11422
+ }
11423
+ controller;
11424
+ layerId;
11425
+ anchor;
11426
+ focus;
11427
+ changed = false;
11428
+ listeners = /* @__PURE__ */ new Set();
11429
+ error = null;
11430
+ get layer() {
11431
+ const l = findLayer(this.controller.getLayers(), this.layerId)?.layer;
11432
+ if (!l || l.type !== "type") throw new Error("The type layer being edited is gone.");
11433
+ return l;
11434
+ }
11435
+ get text() {
11436
+ return this.layer.text;
11437
+ }
11438
+ get start() {
11439
+ return Math.min(this.anchor, this.focus);
11440
+ }
11441
+ get end() {
11442
+ return Math.max(this.anchor, this.focus);
11443
+ }
11444
+ /** Whether the layer still exists (it can be deleted or undone away while editing). */
11445
+ get alive() {
11446
+ return findLayer(this.controller.getLayers(), this.layerId)?.layer.type === "type";
11447
+ }
11448
+ subscribe(fn) {
11449
+ this.listeners.add(fn);
11450
+ return () => this.listeners.delete(fn);
11451
+ }
11452
+ emit() {
11453
+ for (const l of this.listeners) l();
11454
+ }
11455
+ layout() {
11456
+ return this.controller.getTextLayout(this.layerId);
11457
+ }
11458
+ run(fn) {
11459
+ this.error = null;
11460
+ try {
11461
+ const out = fn(!this.changed);
11462
+ this.changed = true;
11463
+ return out;
11464
+ } catch (e) {
11465
+ this.error = e instanceof Error ? e.message : String(e);
11466
+ return void 0;
11467
+ } finally {
11468
+ this.emit();
11469
+ }
11470
+ }
11471
+ /** Replaces the selection with `text` (typing and pasting). */
11472
+ insert(text) {
11473
+ if (!text) return;
11474
+ const s = this.start;
11475
+ const e = this.end;
11476
+ const done = this.run((commit) => this.controller.setText(this.layerId, text, { start: s, end: e }, commit));
11477
+ if (done) this.anchor = this.focus = s + text.replace(/\r\n?/g, "\n").length;
11478
+ }
11479
+ /** Backspace: the selection, else the character before the caret. */
11480
+ deleteBackward() {
11481
+ if (this.start === this.end && this.start === 0) return;
11482
+ const s = this.start === this.end ? Math.max(0, this.start - this.surrogate(this.start - 1, -1)) : this.start;
11483
+ const e = this.end;
11484
+ if (this.run((commit) => this.controller.setText(this.layerId, "", { start: s, end: e }, commit))) this.anchor = this.focus = s;
11485
+ }
11486
+ /** Delete: the selection, else the character after the caret. */
11487
+ deleteForward() {
11488
+ const n = this.text.length;
11489
+ if (this.start === this.end && this.end >= n) return;
11490
+ const s = this.start;
11491
+ const e = this.start === this.end ? Math.min(n, this.end + this.surrogate(this.end, 1)) : this.end;
11492
+ if (this.run((commit) => this.controller.setText(this.layerId, "", { start: s, end: e }, commit))) this.anchor = this.focus = s;
11493
+ }
11494
+ /** 2 when the code unit at `i` is half of a surrogate pair (moving `dir`), else 1. */
11495
+ surrogate(i, dir) {
11496
+ const t = this.text;
11497
+ const c = t.charCodeAt(i);
11498
+ if (dir < 0 && c >= 56320 && c <= 57343 && i > 0) return 2;
11499
+ if (dir > 0 && c >= 55296 && c <= 56319 && i + 1 < t.length) return 2;
11500
+ return 1;
11501
+ }
11502
+ /** Moves the caret (Shift extends the selection). */
11503
+ move(to, extend = false) {
11504
+ const n = this.text.length;
11505
+ let f = this.focus;
11506
+ const collapsedJump = !extend && this.start !== this.end;
11507
+ if (to === "left") f = collapsedJump ? this.start : Math.max(0, f - this.surrogate(f - 1, -1));
11508
+ else if (to === "right") f = collapsedJump ? this.end : Math.min(n, f + this.surrogate(f, 1));
11509
+ else if (to === "start") f = 0;
11510
+ else if (to === "finish") f = n;
11511
+ else {
11512
+ const layout = this.layout();
11513
+ if (layout && layout.lines.length) {
11514
+ const c = caretAt(layout, f);
11515
+ const line = layout.lines[c.line];
11516
+ if (to === "home") f = line.start;
11517
+ else if (to === "end") {
11518
+ const last = line.chars[line.chars.length - 1];
11519
+ f = last ? Math.min(n, last.index + 1) : line.start;
11520
+ if (last && this.text[last.index] === " " && c.line < layout.lines.length - 1) f = last.index;
11521
+ } else {
11522
+ const target = layout.lines[c.line + (to === "up" ? -1 : 1)];
11523
+ if (!target) f = to === "up" ? 0 : n;
11524
+ else {
11525
+ const hit = target.chars.find((ch) => c.x < ch.x + ch.advance / 2);
11526
+ const last = target.chars[target.chars.length - 1];
11527
+ f = hit ? hit.index : last ? last.index + 1 : target.start;
11528
+ }
11529
+ }
11530
+ }
11531
+ }
11532
+ this.focus = f;
11533
+ if (!extend) this.anchor = f;
11534
+ this.emit();
11535
+ }
11536
+ selectAll() {
11537
+ this.anchor = 0;
11538
+ this.focus = this.text.length;
11539
+ this.emit();
11540
+ }
11541
+ /** Places the caret at a document point (Shift-click extends). */
11542
+ pointAt(p, extend = false) {
11543
+ this.focus = this.controller.textIndexAt(this.layerId, p);
11544
+ if (!extend) this.anchor = this.focus;
11545
+ this.emit();
11546
+ }
11547
+ /** Handles a key; returns `true` when it was the session's (the caller then prevents the default). */
11548
+ handleKey(e) {
11549
+ const mod2 = e.metaKey || e.ctrlKey;
11550
+ if (mod2 && e.key.toLowerCase() === "a") {
11551
+ this.selectAll();
11552
+ return true;
11553
+ }
11554
+ switch (e.key) {
11555
+ case "Backspace":
11556
+ this.deleteBackward();
11557
+ return true;
11558
+ case "Delete":
11559
+ this.deleteForward();
11560
+ return true;
11561
+ case "Enter":
11562
+ this.insert("\n");
11563
+ return true;
11564
+ case "ArrowLeft":
11565
+ this.move(mod2 ? "home" : "left", e.shiftKey);
11566
+ return true;
11567
+ case "ArrowRight":
11568
+ this.move(mod2 ? "end" : "right", e.shiftKey);
11569
+ return true;
11570
+ case "ArrowUp":
11571
+ this.move(mod2 ? "start" : "up", e.shiftKey);
11572
+ return true;
11573
+ case "ArrowDown":
11574
+ this.move(mod2 ? "finish" : "down", e.shiftKey);
11575
+ return true;
11576
+ case "Home":
11577
+ this.move("home", e.shiftKey);
11578
+ return true;
11579
+ case "End":
11580
+ this.move("end", e.shiftKey);
11581
+ return true;
11582
+ case "Tab":
11583
+ this.insert(" ");
11584
+ return true;
11585
+ }
11586
+ if (!mod2 && !e.altKey && e.key.length >= 1 && [...e.key].length === 1) {
11587
+ this.insert(e.key);
11588
+ return true;
11589
+ }
11590
+ return false;
11591
+ }
11592
+ /** Sets character style fields on the selection, or on the whole text when nothing is selected. */
11593
+ setStyle(style) {
11594
+ const s = this.start;
11595
+ const e = this.end;
11596
+ const range = s === e ? {} : { start: s, end: e };
11597
+ this.run((commit) => this.controller.setTextStyle(this.layerId, style, range, commit));
11598
+ }
11599
+ /** Sets paragraph fields on the paragraphs the selection touches. */
11600
+ setParagraph(style) {
11601
+ const s = this.start;
11602
+ const e = this.end;
11603
+ this.run((commit) => this.controller.setParagraphStyle(this.layerId, style, { start: s, end: e }, commit));
11604
+ }
11605
+ /** The style at the caret (or the selection's start), for the options bar. */
11606
+ styleHere() {
11607
+ const layer = this.layer;
11608
+ const at = this.start > 0 && this.start === this.end ? this.start - 1 : this.start;
11609
+ return styleAt(layer.runs, at, DEFAULT_TEXT_STYLE);
11610
+ }
11611
+ info() {
11612
+ return this.controller.getTextLayer(this.layerId);
11613
+ }
11614
+ /** The caret, the selection and the frame in natural pixels, for `RenderOptions.textEdit`. */
11615
+ overlay() {
11616
+ const layer = this.layer;
11617
+ const layout = this.layout();
11618
+ const m = textMatrix(layer);
11619
+ const at = (x, y) => applyMatrix(m, { x, y });
11620
+ let frame = null;
11621
+ if (layer.frame.kind === "box") frame = [at(0, 0), at(layer.frame.width, 0), at(layer.frame.width, layer.frame.height), at(0, layer.frame.height)];
11622
+ else if (layout) {
11623
+ const b = layout.bounds;
11624
+ frame = [at(b.x, b.y), at(b.x + b.width, b.y), at(b.x + b.width, b.y + b.height), at(b.x, b.y + b.height)];
11625
+ }
11626
+ if (!layout) return { caret: null, selection: [], frame };
11627
+ const selection = [];
11628
+ if (this.start !== this.end) {
11629
+ for (const l of layout.lines) {
11630
+ const chars = l.chars.filter((c2) => c2.index >= this.start && c2.index < this.end);
11631
+ if (!chars.length) continue;
11632
+ const x0 = chars[0].x;
11633
+ const x1 = chars[chars.length - 1].x + chars[chars.length - 1].advance;
11634
+ const top = l.baseline - l.ascent;
11635
+ const bottom = l.baseline + l.descent;
11636
+ selection.push([at(x0, top), at(x1, top), at(x1, bottom), at(x0, bottom)]);
11637
+ }
11638
+ }
11639
+ const c = caretAt(layout, this.focus);
11640
+ return { caret: [at(c.x, c.top), at(c.x, c.bottom)], selection, frame };
11641
+ }
11642
+ /** Ends editing: an empty layer made for this session is deleted (as Photoshop does). Returns whether the layer remains. */
11643
+ close() {
11644
+ if (!this.alive) return false;
11645
+ if (!this.text.trim().length) {
11646
+ try {
11647
+ this.controller.removeLayer(this.layerId);
11648
+ } catch {
11649
+ }
11650
+ return false;
11651
+ }
11652
+ if (this.layer.name === "Text" && this.changed) {
11653
+ try {
11654
+ this.controller.setLayerProperties(this.layerId, { name: typeLayerName(this.text) }, false);
11655
+ } catch {
11656
+ }
11657
+ }
11658
+ return true;
11659
+ }
11660
+ };
11661
+ function typeLayerAt(controller, p, slop = 4) {
11662
+ const layers = [...flattenTypeLayers(controller.getLayers())].reverse();
11663
+ for (const l of layers) {
11664
+ if (!l.visible) continue;
11665
+ const inv = invert(textMatrix(l));
11666
+ const q = applyMatrix(inv, p);
11667
+ let b = null;
11668
+ if (l.frame.kind === "box") b = { x: 0, y: 0, width: l.frame.width, height: l.frame.height };
11669
+ else {
11670
+ const layout = controller.getTextLayout(l.id);
11671
+ if (layout) b = layout.bounds;
11672
+ }
11673
+ if (b && q.x >= b.x - slop && q.x <= b.x + b.width + slop && q.y >= b.y - slop && q.y <= b.y + b.height + slop) return l.id;
11674
+ }
11675
+ return null;
11676
+ }
11677
+ function flattenTypeLayers(layers) {
11678
+ const out = [];
11679
+ const walk = (list) => {
11680
+ for (const l of list) {
11681
+ if (l.type === "type") out.push(l);
11682
+ else if (l.type === "group") walk(l.children);
11683
+ }
11684
+ };
11685
+ walk(layers);
11686
+ return out;
11687
+ }
11688
+ function invert(m) {
11689
+ const [a, b, c, d, e, f] = m;
11690
+ const det = a * d - b * c || 1;
11691
+ return [d / det, -b / det, -c / det, a / det, (c * f - d * e) / det, (b * e - a * f) / det];
11692
+ }
11693
+ var TypeToolGesture = class {
11694
+ constructor(controller, options) {
11695
+ this.controller = controller;
11696
+ this.options = options;
11697
+ }
11698
+ controller;
11699
+ options;
11700
+ session = null;
11701
+ dragStart = null;
11702
+ error = null;
11703
+ down(p, mods = {}) {
11704
+ this.error = null;
11705
+ const hit = typeLayerAt(this.controller, p);
11706
+ if (this.session && this.session.alive && hit === this.session.layerId) {
11707
+ this.session.pointAt(p, mods.shift);
11708
+ this.dragStart = null;
11709
+ return;
11710
+ }
11711
+ this.finish();
11712
+ if (hit) {
11713
+ this.controller.setActiveLayer(hit);
11714
+ this.session = new TextEditSession(this.controller, hit, this.controller.textIndexAt(hit, p));
11715
+ this.dragStart = null;
11716
+ return;
11717
+ }
11718
+ this.dragStart = { x: p.x, y: p.y };
11719
+ }
11720
+ /** Releases a click or drag that did not land on type: adds a new text layer (point text, or a box for a drag). */
11721
+ up(p) {
11722
+ const start = this.dragStart;
11723
+ this.dragStart = null;
11724
+ if (!start) return this.session;
11725
+ const o = this.options();
11726
+ const w = Math.abs(p.x - start.x);
11727
+ const h = Math.abs(p.y - start.y);
11728
+ try {
11729
+ const box = w >= 8 && h >= 8 ? { width: w, height: h } : void 0;
11730
+ const id = this.controller.addTextLayer({
11731
+ text: "",
11732
+ x: box ? Math.min(start.x, p.x) : start.x,
11733
+ y: box ? Math.min(start.y, p.y) : start.y,
11734
+ ...box ? { box } : {},
11735
+ style: { fontFamily: o.fontFamily, fontSize: o.fontSize, fontWeight: o.fontWeight, italic: o.italic, color: o.color },
11736
+ paragraph: o.align !== "left" ? { align: o.align } : {},
11737
+ name: "Text"
11738
+ });
11739
+ this.session = new TextEditSession(this.controller, id, 0);
11740
+ } catch (e) {
11741
+ this.error = e instanceof Error ? e.message : String(e);
11742
+ }
11743
+ return this.session;
11744
+ }
11745
+ /** Ends the open session (an empty new layer is removed). */
11746
+ finish() {
11747
+ const s = this.session;
11748
+ this.session = null;
11749
+ s?.close();
11750
+ }
11751
+ };
11752
+
10564
11753
  // src/image-editor/layers/panel.ts
10565
11754
  function layerPanelRows(layers, activeId) {
10566
11755
  const rows = [];
@@ -10601,7 +11790,18 @@ function layerBadge(layer) {
10601
11790
  return "so";
10602
11791
  case "shape":
10603
11792
  return "shp";
11793
+ case "type":
11794
+ return "T";
11795
+ }
11796
+ }
11797
+ function layerHasThumbnail(layer) {
11798
+ return layer.type === "pixel" || layer.type === "shape" || layer.type === "type";
11799
+ }
11800
+ function layerWarning(layer) {
11801
+ if (layer.type === "type" && layer.missingFonts?.length) {
11802
+ return `Missing ${layer.missingFonts.length === 1 ? "font" : "fonts"}: ${layer.missingFonts.join(", ")}. ${layer.rendering === "stored" ? "Showing the saved pixels until the text is edited." : "Drawn with a fallback font."}`;
10604
11803
  }
11804
+ return null;
10605
11805
  }
10606
11806
  function dropZone(fraction, isGroup) {
10607
11807
  if (isGroup && fraction > 0.3 && fraction < 0.7) return "inside";
@@ -10640,7 +11840,7 @@ function layerMenu(controller) {
10640
11840
  { action: "delete-mask", label: "Delete Layer Mask", enabled: Boolean(active?.mask) },
10641
11841
  { action: "toggle-mask", label: active?.mask?.enabled === false ? "Enable Layer Mask" : "Disable Layer Mask", enabled: Boolean(active?.mask) },
10642
11842
  { action: "toggle-mask-link", label: active?.mask?.linked === false ? "Link Layer Mask" : "Unlink Layer Mask", enabled: Boolean(active?.mask && !active.mask.source) },
10643
- { action: "rasterize", label: "Rasterize Layer", enabled: active?.type === "annotation" || active?.type === "smart" || active?.type === "shape" || isPixel && Boolean(active?.type === "pixel" && active.filters?.length) },
11843
+ { 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) },
10644
11844
  { action: "convert-smart", label: "Convert to Smart Object", enabled: Boolean(active && active.type !== "annotation") },
10645
11845
  { action: "edit-contents", label: "Edit Contents", enabled: active?.type === "smart" },
10646
11846
  { action: "smart-via-copy", label: "New Smart Object via Copy", enabled: active?.type === "smart" },
@@ -11573,6 +12773,7 @@ export {
11573
12773
  D50,
11574
12774
  D65,
11575
12775
  DEFAULT_ADJUSTMENTS,
12776
+ DEFAULT_AUTO_LEADING,
11576
12777
  DEFAULT_BLENDER,
11577
12778
  DEFAULT_BRUSH,
11578
12779
  DEFAULT_DEPTH_MASK,
@@ -11581,8 +12782,11 @@ export {
11581
12782
  DEFAULT_MIXER,
11582
12783
  DEFAULT_REFINE_EDGE_PARAMS,
11583
12784
  DEFAULT_SELECTION_TOOL_OPTIONS,
12785
+ DEFAULT_SHAPE_TOOL_OPTIONS,
11584
12786
  DEFAULT_SMUDGE,
12787
+ DEFAULT_TEXT_STYLE,
11585
12788
  DEFAULT_TRANSFORM,
12789
+ DEFAULT_TYPE_TOOL_OPTIONS,
11586
12790
  DEPTH_MASK_CONTROLS,
11587
12791
  DEVELOP_FILTER_ID,
11588
12792
  DIRECT_GAUSSIAN_MAX_SIGMA,
@@ -11631,6 +12835,7 @@ export {
11631
12835
  MixerStroke,
11632
12836
  PAINTERLY_BLEND_MODES,
11633
12837
  PAINT_TOOLS,
12838
+ PARAGRAPH_STYLE_FIELDS,
11634
12839
  PATTERN_BLOCK_KEYS,
11635
12840
  PIGMENT_BANDS,
11636
12841
  PIGMENT_LAMBDA_MIN,
@@ -11650,23 +12855,30 @@ export {
11650
12855
  SELECTION_MODIFY_OPS,
11651
12856
  SELECTION_TOOLS,
11652
12857
  SHADOW_AMBIENT,
12858
+ SHAPE_KIND_LABELS,
11653
12859
  SHAPE_OPERATIONS,
11654
12860
  SHAPE_RENDER_MARGIN,
11655
12861
  SMART_RENDER_MARGIN,
11656
12862
  SUPPORTED_ADJUSTMENT_KEYS,
11657
12863
  SelectionGesture,
12864
+ ShapeDrawGesture,
11658
12865
  SmudgeTip,
11659
12866
  StrokeCoverage,
11660
12867
  SurfaceDraft,
12868
+ TEXT_ALIGNS,
12869
+ TEXT_RENDER_MARGIN,
12870
+ TEXT_STYLE_FIELDS,
11661
12871
  TILE,
11662
12872
  TILE_SHIFT,
11663
12873
  TINT_SCALE,
11664
12874
  TONE_LUT_SIZE,
11665
12875
  TONE_RANGE_PRESETS,
11666
12876
  TRANSFORM_ANCHORS,
12877
+ TextEditSession,
11667
12878
  TileMedium,
11668
12879
  TiledSurface,
11669
12880
  TransformTooLargeError,
12881
+ TypeToolGesture,
11670
12882
  VIBRANCE_GREY,
11671
12883
  WARP_GRID_OPTIONS,
11672
12884
  WARP_STYLES,
@@ -11691,6 +12903,7 @@ export {
11691
12903
  annotationLayerName,
11692
12904
  annotationOfLayer,
11693
12905
  annotationOfShape,
12906
+ annotationOfType,
11694
12907
  applyAdjustmentLayers,
11695
12908
  applyAdjustments,
11696
12909
  applyBoxBlur,
@@ -11704,6 +12917,7 @@ export {
11704
12917
  applyProcess2Adjustments,
11705
12918
  applyRegionBlur,
11706
12919
  applyRegionMosaic,
12920
+ applyShapeOptions,
11707
12921
  applySmartFilters,
11708
12922
  applyStrokeToTile,
11709
12923
  backgroundLayer,
@@ -11733,6 +12947,7 @@ export {
11733
12947
  calculateCropDrag,
11734
12948
  canvasCompositeOperation,
11735
12949
  canvasToImagePoint,
12950
+ caretAt,
11736
12951
  changedTiles,
11737
12952
  clampControlValue,
11738
12953
  clampCropToImage,
@@ -11760,6 +12975,7 @@ export {
11760
12975
  coveringRect,
11761
12976
  createAdjustmentLayer,
11762
12977
  createBlenderScratch,
12978
+ createCanvasTextShaper,
11763
12979
  createImageCaptureSurface,
11764
12980
  createImageEditTools,
11765
12981
  createImageEditorToolProvider,
@@ -11771,10 +12987,15 @@ export {
11771
12987
  createImageShapeTools,
11772
12988
  createImageSmartObjectTools,
11773
12989
  createImageTransformTools,
12990
+ createImageTypeTools,
12991
+ createOutlineTextShaper,
11774
12992
  createPigment,
11775
12993
  createPigmentTools,
11776
12994
  createSelectionMask,
11777
12995
  createShapeLayer,
12996
+ createTypeLayer,
12997
+ cssFont,
12998
+ cssFontFamily,
11778
12999
  curveLut,
11779
13000
  curveLutFromLevels,
11780
13001
  curvePointToBox,
@@ -11843,9 +13064,12 @@ export {
11843
13064
  findLayer,
11844
13065
  findLayerControl,
11845
13066
  finishTile,
13067
+ fitParagraphRuns,
13068
+ fitRuns,
11846
13069
  flattenLayers,
11847
13070
  flattenPath,
11848
13071
  flipCorners,
13072
+ fontOf,
11849
13073
  formatControlValue,
11850
13074
  fullSelection,
11851
13075
  gaussianBlur,
@@ -11926,6 +13150,7 @@ export {
11926
13150
  layerEffectsBadge,
11927
13151
  layerEffectsOf,
11928
13152
  layerEffectsPanel,
13153
+ layerHasThumbnail,
11929
13154
  layerLabel,
11930
13155
  layerMaskCoverage,
11931
13156
  layerMaskFromSelection,
@@ -11935,7 +13160,9 @@ export {
11935
13160
  layerPanelRows,
11936
13161
  layerTree,
11937
13162
  layerTypeLabel,
13163
+ layerWarning,
11938
13164
  layersFromLegacy,
13165
+ layoutText,
11939
13166
  legacyAdjustmentLayer,
11940
13167
  legacyMaskOf,
11941
13168
  legacyView,
@@ -11945,6 +13172,7 @@ export {
11945
13172
  linearSrgbToXy,
11946
13173
  linearToSrgb,
11947
13174
  liveComponent,
13175
+ liveShapeForDrag,
11948
13176
  liveShapeSubpaths,
11949
13177
  loadImageEditorDocument,
11950
13178
  loadMixer,
@@ -11995,11 +13223,13 @@ export {
11995
13223
  normalizeSmudge,
11996
13224
  normalizeStoredAdjustments,
11997
13225
  normalizeSubpaths,
13226
+ normalizeText,
11998
13227
  offsetLayer,
11999
13228
  offsetShape,
12000
13229
  offsetSmartObject,
12001
13230
  openPsdFile,
12002
13231
  packRgba,
13232
+ paragraphRanges,
12003
13233
  parseAdjustmentBlock,
12004
13234
  parseBlend,
12005
13235
  parseColor,
@@ -12011,6 +13241,8 @@ export {
12011
13241
  parsePatternBlock,
12012
13242
  parseSelectionMode,
12013
13243
  parseVersionedDescriptor,
13244
+ patchParagraphStyle,
13245
+ patchTextStyle,
12014
13246
  photoFilterMatrix,
12015
13247
  pigment8,
12016
13248
  pixelLayerBounds,
@@ -12066,19 +13298,24 @@ export {
12066
13298
  renderShape,
12067
13299
  renderShapeRect,
12068
13300
  renderSmartObject,
13301
+ renderText,
12069
13302
  replaceContents,
13303
+ replaceTextRange,
12070
13304
  requireLayer,
12071
13305
  requireOperation,
12072
13306
  requireShapeLayer,
12073
13307
  requireSmartObject,
13308
+ requireTypeLayer,
12074
13309
  requireUnlocked,
12075
13310
  rerender,
12076
13311
  rerenderShape,
13312
+ rerenderText,
12077
13313
  resizePixels,
12078
13314
  resizeSelection,
12079
13315
  resizeWarpMesh,
12080
13316
  resolveExternalAssets,
12081
13317
  resolveMosaicRegionBounds,
13318
+ restyleRange,
12082
13319
  rgbToGray,
12083
13320
  rgbToHsl,
12084
13321
  rgbToReflectance,
@@ -12125,16 +13362,22 @@ export {
12125
13362
  smoothCoverage,
12126
13363
  smoothPoints,
12127
13364
  solidSurface,
13365
+ spliceRuns,
12128
13366
  splitOverlayAnnotations,
12129
13367
  srgb8ToLab,
12130
13368
  srgbToLinear,
12131
13369
  standardBlendMode,
13370
+ styleAt,
12132
13371
  sumHistograms,
12133
13372
  surfaceBytes,
12134
13373
  surfaceFromPixels,
12135
13374
  surfacesEqual,
12136
13375
  tempTintToXy,
12137
13376
  temperatureToKelvin,
13377
+ textIndexAt,
13378
+ textMatrix,
13379
+ textOutlines,
13380
+ textRange,
12138
13381
  tileCoord,
12139
13382
  tileKey,
12140
13383
  tileRect,
@@ -12143,6 +13386,7 @@ export {
12143
13386
  tipReach,
12144
13387
  tipShape,
12145
13388
  toHex,
13389
+ toTextSpace,
12146
13390
  toneCurve,
12147
13391
  toneLuts,
12148
13392
  toneRange,
@@ -12150,10 +13394,16 @@ export {
12150
13394
  transformMap,
12151
13395
  transformShapeComponents,
12152
13396
  transformSurface,
13397
+ transformTypeLayer,
12153
13398
  transformedBounds,
12154
13399
  transformedBox,
12155
13400
  transformedPoint,
12156
13401
  translateSurface,
13402
+ typeAsPixelLayer,
13403
+ typeFrame,
13404
+ typeFromAnnotation,
13405
+ typeLayerAt,
13406
+ typeLayerName,
12157
13407
  ungroupLayer,
12158
13408
  uniformTile,
12159
13409
  unionRect,
@@ -12163,6 +13413,7 @@ export {
12163
13413
  updateLayerMaskProps,
12164
13414
  updateShapeLayer,
12165
13415
  updateSmartFilter,
13416
+ updateTypeLayer,
12166
13417
  uvToXy,
12167
13418
  validateEffectGradient,
12168
13419
  validateLayerEffect,