@pixerate/editor 0.110.0 → 0.111.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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-HHZ6ZJTW.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 {
@@ -2441,6 +2479,159 @@ function refineSelection(pixels, mask, params = {}) {
2441
2479
  return createSelectionMask(mask.width, mask.height, out);
2442
2480
  }
2443
2481
 
2482
+ // src/image-editor/layers/text-shapers.ts
2483
+ var GENERIC = /* @__PURE__ */ new Set(["serif", "sans-serif", "monospace", "cursive", "fantasy", "system-ui", "ui-serif", "ui-sans-serif", "ui-monospace", "ui-rounded", "math", "emoji", "fangsong"]);
2484
+ function cssFontFamily(family) {
2485
+ const f = family.trim();
2486
+ if (GENERIC.has(f.toLowerCase()) || f.includes(",") || /^["']/.test(f)) return f;
2487
+ return /^[A-Za-z_-][\w -]*$/.test(f) && !/\s{2}/.test(f) ? f : `"${f.replace(/"/g, '\\"')}"`;
2488
+ }
2489
+ function cssFont(font, family = font.family) {
2490
+ return `${font.italic ? "italic " : ""}${font.weight} ${font.size}px ${cssFontFamily(family)}`;
2491
+ }
2492
+ function createCanvasTextShaper(options = {}) {
2493
+ const make = options.createCanvas ?? ((w, h) => {
2494
+ if (typeof OffscreenCanvas !== "undefined") return new OffscreenCanvas(w, h);
2495
+ if (typeof document !== "undefined") {
2496
+ const c = document.createElement("canvas");
2497
+ c.width = w;
2498
+ c.height = h;
2499
+ return c;
2500
+ }
2501
+ return null;
2502
+ });
2503
+ const canvas = make(1, 1);
2504
+ const ctx = canvas?.getContext("2d");
2505
+ if (!ctx || typeof ctx.measureText !== "function") return null;
2506
+ const fallback = options.fallbackFamily ?? "sans-serif";
2507
+ const available = /* @__PURE__ */ new Map();
2508
+ const widths = /* @__PURE__ */ new Map();
2509
+ const metrics = /* @__PURE__ */ new Map();
2510
+ const probe = "mmmmmmmmmmlli1WQ@#";
2511
+ const width = (font, text) => {
2512
+ ctx.font = font;
2513
+ return ctx.measureText(text).width;
2514
+ };
2515
+ const isAvailable = (family) => {
2516
+ const f = family.trim();
2517
+ if (GENERIC.has(f.toLowerCase()) || f.includes(",")) return true;
2518
+ let hit = available.get(f);
2519
+ if (hit === void 0) {
2520
+ const q = cssFontFamily(f);
2521
+ hit = width(`72px ${q}, monospace`, probe) !== width("72px monospace", probe) || width(`72px ${q}, serif`, probe) !== width("72px serif", probe);
2522
+ available.set(f, hit);
2523
+ }
2524
+ return hit;
2525
+ };
2526
+ return {
2527
+ id: "canvas",
2528
+ resolve(font) {
2529
+ const ok = isAvailable(font.family);
2530
+ return { family: ok ? font.family : fallback, available: ok };
2531
+ },
2532
+ metrics(font) {
2533
+ const key = cssFont(font);
2534
+ let m = metrics.get(key);
2535
+ if (!m) {
2536
+ ctx.font = key;
2537
+ ctx.textBaseline = "alphabetic";
2538
+ const t = ctx.measureText("Hg");
2539
+ const ascent = t.emHeightAscent ?? t.fontBoundingBoxAscent ?? font.size * 0.8;
2540
+ const descent = t.emHeightDescent ?? t.fontBoundingBoxDescent ?? font.size * 0.2;
2541
+ m = { ascent: Number.isFinite(ascent) ? ascent : font.size * 0.8, descent: Number.isFinite(descent) ? descent : font.size * 0.2 };
2542
+ metrics.set(key, m);
2543
+ }
2544
+ return m;
2545
+ },
2546
+ measure(text, font) {
2547
+ const key = `${cssFont(font)}|${text}`;
2548
+ let w = widths.get(key);
2549
+ if (!w) {
2550
+ ctx.font = cssFont(font);
2551
+ w = [0];
2552
+ for (let i = 1; i <= text.length; i++) w.push(ctx.measureText(text.slice(0, i)).width);
2553
+ if (widths.size > 2e3) widths.clear();
2554
+ widths.set(key, w);
2555
+ }
2556
+ return w;
2557
+ },
2558
+ draw(target, text, font, x, y) {
2559
+ target.font = cssFont(font);
2560
+ target.textBaseline = "alphabetic";
2561
+ target.textAlign = "left";
2562
+ target.fillText(text, x, y);
2563
+ },
2564
+ families() {
2565
+ const out = /* @__PURE__ */ new Set(["sans-serif", "serif", "monospace"]);
2566
+ const fonts = typeof document !== "undefined" ? document.fonts : void 0;
2567
+ if (fonts && typeof fonts.forEach === "function") fonts.forEach((f) => out.add(f.family.replace(/^["']|["']$/g, "")));
2568
+ return [...out];
2569
+ }
2570
+ };
2571
+ }
2572
+ function createOutlineTextShaper(fonts, options = {}) {
2573
+ if (!fonts.length) throw new Error("createOutlineTextShaper needs at least one font.");
2574
+ const entries = fonts.map((f) => {
2575
+ const e = "face" in f ? f : { face: f };
2576
+ const names = e.face.names;
2577
+ const family = e.family ?? names?.fontFamily?.en ?? Object.values(names?.fontFamily ?? {})[0] ?? "Font";
2578
+ const weight = e.weight ?? e.face.tables?.os2?.usWeightClass ?? 400;
2579
+ const italic = e.italic ?? Boolean((e.face.tables?.os2?.fsSelection ?? 0) & 1);
2580
+ const postScript = names?.postScriptName?.en ?? Object.values(names?.postScriptName ?? {})[0];
2581
+ return { face: e.face, family, key: family.toLowerCase(), weight, italic, postScript };
2582
+ });
2583
+ const fallbackKey = (options.fallback ?? entries[0].family).toLowerCase();
2584
+ const pick2 = (font) => {
2585
+ if (font.postScriptName) {
2586
+ const exact = entries.find((e) => e.postScript === font.postScriptName);
2587
+ if (exact) return { entry: exact, available: true };
2588
+ }
2589
+ const key = font.family.trim().toLowerCase();
2590
+ let pool = entries.filter((e) => e.key === key);
2591
+ const available = pool.length > 0;
2592
+ if (!available) pool = entries.filter((e) => e.key === fallbackKey);
2593
+ if (!pool.length) pool = entries;
2594
+ const styled = pool.filter((e) => e.italic === font.italic);
2595
+ const list = styled.length ? styled : pool;
2596
+ const entry = list.reduce((best, e) => Math.abs(e.weight - font.weight) < Math.abs(best.weight - font.weight) ? e : best);
2597
+ return { entry, available };
2598
+ };
2599
+ const toPath = (cmds) => {
2600
+ const out = [];
2601
+ for (const c of cmds) {
2602
+ if (c.type === "M" || c.type === "L") out.push({ type: c.type, x: c.x, y: c.y });
2603
+ else if (c.type === "C") out.push({ type: "C", x1: c.x1, y1: c.y1, x2: c.x2, y2: c.y2, x: c.x, y: c.y });
2604
+ else if (c.type === "Q") out.push({ type: "Q", x1: c.x1, y1: c.y1, x: c.x, y: c.y });
2605
+ else out.push({ type: "Z" });
2606
+ }
2607
+ return out;
2608
+ };
2609
+ return {
2610
+ id: options.id ?? `outline:${entries.map((e) => `${e.family}/${e.weight}${e.italic ? "i" : ""}`).join(",")}`,
2611
+ resolve(font) {
2612
+ const { entry, available } = pick2(font);
2613
+ return { family: entry.family, available };
2614
+ },
2615
+ metrics(font) {
2616
+ const { face } = pick2(font).entry;
2617
+ const k = font.size / face.unitsPerEm;
2618
+ return { ascent: face.ascender * k, descent: -face.descender * k };
2619
+ },
2620
+ measure(text, font) {
2621
+ const { face } = pick2(font).entry;
2622
+ const out = [0];
2623
+ for (let i = 1; i <= text.length; i++) out.push(face.getAdvanceWidth(text.slice(0, i), font.size, { kerning: true }));
2624
+ return out;
2625
+ },
2626
+ outline(text, font, x, y) {
2627
+ return toPath(pick2(font).entry.face.getPath(text, x, y, font.size, { kerning: true }).commands);
2628
+ },
2629
+ families() {
2630
+ return [...new Set(entries.map((e) => e.family))];
2631
+ }
2632
+ };
2633
+ }
2634
+
2444
2635
  // src/image-editor/free-transform.ts
2445
2636
  var FREE_TRANSFORM_CONTENTS = ["auto", "layer", "selection", "selection-copy", "selection-outline"];
2446
2637
  var fail = (message) => {
@@ -2512,7 +2703,7 @@ var FreeTransformSession = class {
2512
2703
  }
2513
2704
  const targetId = init.layerId ?? host.activeLayerId();
2514
2705
  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";
2706
+ const smartTarget = (target?.type === "smart" || target?.type === "shape" || target?.type === "type") && (init.channel ?? (target.id === host.activeLayerId() ? host.activeTarget() : "pixels")) !== "mask";
2516
2707
  if (content === "auto") content = selected && !smartTarget ? "selection" : "layer";
2517
2708
  this.content = content;
2518
2709
  if (content !== "layer" && !selected) fail(`Nothing is selected; ${content} needs a selection. Select first, or use content layer.`);
@@ -2539,7 +2730,7 @@ var FreeTransformSession = class {
2539
2730
  const linked = mask?.linked !== false;
2540
2731
  const maskAlone = channel === "mask" && (!linked || layer.type !== "pixel");
2541
2732
  if (channel === "mask" && !mask) fail(layer.mask?.source ? `Layer "${layer.name || id}" has a depth mask, which is generated, not transformed.` : `Layer "${layer.name || id}" has no mask to transform; add one with add_layer_mask, or transform its pixels.`);
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.`);
2733
+ if (!maskAlone && layer.type !== "pixel" && layer.type !== "smart" && layer.type !== "shape" && layer.type !== "type") fail(`Layer "${layer.name || id}" is a ${layer.type} layer; Free Transform works on pixel, shape, type and smart object layers, layer masks (channel mask) and selections.`);
2543
2734
  if (!layer.visible && content !== "selection-copy") fail(`Layer "${layer.name || id}" is hidden; show it before you transform it.`);
2544
2735
  if (content !== "selection-copy") {
2545
2736
  requireUnlocked(layer, "position", "transform it");
@@ -2561,6 +2752,26 @@ var FreeTransformSession = class {
2561
2752
  this.shown = this.startLayers;
2562
2753
  return;
2563
2754
  }
2755
+ if (layer.type === "type" && !maskAlone) {
2756
+ if (content !== "layer") fail(`Layer "${layer.name || id}" is a type layer, which transforms as a whole; use content layer (or rasterize it to transform part of it).`);
2757
+ const frame = typeFrame(layer, host.textEnv().shaper);
2758
+ const m = textMatrix(layer);
2759
+ const at = (x, y) => ({ x: m[0] * x + m[2] * y + m[4], y: m[1] * x + m[3] * y + m[5] });
2760
+ this.typeTarget = layer;
2761
+ this.typeLayer = true;
2762
+ this.box = frame;
2763
+ this.startGeometry = {
2764
+ box: frame,
2765
+ corners: [at(frame.x, frame.y), at(frame.x + frame.width, frame.y), at(frame.x + frame.width, frame.y + frame.height), at(frame.x, frame.y + frame.height)],
2766
+ warp: null
2767
+ };
2768
+ this.geometryValue = this.startGeometry;
2769
+ this.startInterp = this.interp;
2770
+ this.moves = mask && linked ? ["pixels", "mask"] : ["pixels"];
2771
+ if (mask && linked) this.parts.push({ what: "mask", offset: o, moving: mask.surface, base: null, reduced: /* @__PURE__ */ new Map() });
2772
+ this.shown = this.startLayers;
2773
+ return;
2774
+ }
2564
2775
  if (layer.type === "shape" && !maskAlone) {
2565
2776
  if (content !== "layer") fail(`Layer "${layer.name || id}" is a shape layer, which transforms as a whole; use content layer (or rasterize it to transform part of it).`);
2566
2777
  const g = shapeGeometryBounds(layer);
@@ -2663,6 +2874,9 @@ var FreeTransformSession = class {
2663
2874
  /** True when the layer is a shape layer (the transform maps its paths and renders them again). */
2664
2875
  shapeLayer = false;
2665
2876
  shape = null;
2877
+ /** True when the layer is a type layer (the transform sets its text matrix; affine only). */
2878
+ typeLayer = false;
2879
+ typeTarget = null;
2666
2880
  geometryValue;
2667
2881
  smart = null;
2668
2882
  smartReduced = /* @__PURE__ */ new Map();
@@ -2711,6 +2925,9 @@ var FreeTransformSession = class {
2711
2925
  };
2712
2926
  if (next.corners.length !== 4 || !next.corners.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y))) fail("corners must be four finite points: top-left, top-right, bottom-right, bottom-left.");
2713
2927
  if (!transformMap(next)) fail("These corners fold the frame over itself or through the horizon; keep the quad convex.");
2928
+ if (this.typeTarget && (hasWarp(next) || !matrixFromCorners(next.box, next.corners))) {
2929
+ fail("Type layers take scale, rotate, skew and flips only (the corners must stay a parallelogram, no warp); rasterize the layer (rasterize_layer) for distort, perspective or warp.");
2930
+ }
2714
2931
  this.geometryValue = next;
2715
2932
  if (preview !== false) this.render(preview);
2716
2933
  return next;
@@ -2718,6 +2935,7 @@ var FreeTransformSession = class {
2718
2935
  /** Resamples every part for the current geometry and shows it (or returns it, for commit). */
2719
2936
  resample(final, preview = {}) {
2720
2937
  if (this.smart) return { layers: this.resampleSmart(final, preview), selection: this.startSelection };
2938
+ if (this.typeTarget) return { layers: this.resampleType(), selection: this.startSelection };
2721
2939
  const g = this.geometryValue;
2722
2940
  const identity = isIdentityTransform(g);
2723
2941
  const warp = hasWarp(g);
@@ -2798,6 +3016,27 @@ var FreeTransformSession = class {
2798
3016
  const surface = renderShape(moved, this.host.shapeEnv(), final ? {} : { clip: preview.clip });
2799
3017
  return mapLayer(this.startLayers, layer.id, () => ({ ...moved, surface }));
2800
3018
  }
3019
+ /**
3020
+ * A type layer with its text matrix set from the corners (affine only: scale, rotate, skew and
3021
+ * flips), drawn again; a linked mask follows by the change's homography.
3022
+ */
3023
+ resampleType() {
3024
+ const layer = this.typeTarget;
3025
+ const g = this.geometryValue;
3026
+ if (hasWarp(g)) fail("Type layers cannot be warped here; rasterize the layer first (rasterize_layer), or transform it with scale, rotate and skew.");
3027
+ const m = matrixFromCorners(g.box, g.corners);
3028
+ if (!m) fail("Type layers take scale, rotate, skew and flips only (the corners must stay a parallelogram); rasterize the layer for distort or perspective.");
3029
+ const next = { ...layer, transform: m };
3030
+ delete next.annotation;
3031
+ let layers = mapLayer(this.startLayers, layer.id, () => rerenderText(next, this.host.textEnv()));
3032
+ const change = multiplyHomographies(homographyFromAffine(invertMatrix(textMatrix(layer))), homographyFromAffine(m));
3033
+ for (const part of this.parts) {
3034
+ if (part.what !== "mask") continue;
3035
+ const surface = transformSurface(part.moving, change, { interpolation: this.interp === "nearest" ? "nearest" : "bilinear" });
3036
+ layers = mapLayer(layers, layer.id, (l) => ({ ...l, mask: { ...l.mask, surface } }));
3037
+ }
3038
+ return layers;
3039
+ }
2801
3040
  /** A smart object re-rendered from its contents for the current geometry (previews leave the smart filters out). */
2802
3041
  resampleSmart(final, preview) {
2803
3042
  const layer = this.smart;
@@ -2821,7 +3060,7 @@ var FreeTransformSession = class {
2821
3060
  /** Whether the geometry (or the interpolation of a smart object) differs from where it started. */
2822
3061
  isChanged() {
2823
3062
  const g = this.geometryValue;
2824
- if (!this.smart) return !isIdentityTransform(g);
3063
+ if (!this.smart && !this.typeTarget) return !isIdentityTransform(g);
2825
3064
  const a = this.startGeometry;
2826
3065
  const same = g.corners.every((c, i) => Math.abs(c.x - a.corners[i].x) < 1e-9 && Math.abs(c.y - a.corners[i].y) < 1e-9);
2827
3066
  const warp = (w, box) => w && hasWarp({ box, corners: a.corners, warp: w }) ? JSON.stringify(w) : "";
@@ -2858,7 +3097,7 @@ var FreeTransformSession = class {
2858
3097
  const id = this.copyId ?? this.layerId;
2859
3098
  const layer = id ? findLayer(layers, id)?.layer : void 0;
2860
3099
  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);
3100
+ else if (this.moves.includes("pixels") && (layer?.type === "smart" || layer?.type === "shape" || layer?.type === "type")) bounds = contentBounds(layer.surface);
2862
3101
  else if (this.moves.includes("mask") && layer?.mask) bounds = toDoc(contentBounds(layer.mask.surface), layer.type === "pixel" ? layer.offset : { x: 0, y: 0 });
2863
3102
  else if (this.moves.includes("selection")) bounds = selection?.bounds ? { ...selection.bounds } : null;
2864
3103
  return {
@@ -3112,6 +3351,7 @@ var ImageEditorController = class {
3112
3351
  compositor = new LayerCompositor();
3113
3352
  constructor(options = {}) {
3114
3353
  this.maxHistoryDepth = options.maxHistoryDepth ?? 50;
3354
+ if (options.textShaper !== void 0) this.textShaperValue = options.textShaper;
3115
3355
  this.maxHistoryBytes = options.maxHistoryBytes ?? 512 * 1024 * 1024;
3116
3356
  this.onStateChange = options.onStateChange;
3117
3357
  this.renderer = new ImageEditorRenderer();
@@ -3818,6 +4058,21 @@ var ImageEditorController = class {
3818
4058
  const b = shapeGeometryBounds(moved) ?? { x: 0, y: 0 };
3819
4059
  return { offset: { x: Math.round(b.x), y: Math.round(b.y) }, bounds: this.getLayerBounds(id) };
3820
4060
  }
4061
+ if (layer.type === "type") {
4062
+ const m = layer.transform ?? [1, 0, 0, 1, 0, 0];
4063
+ const dx = move.x !== void 0 ? move.x - m[4] : move.dx ?? 0;
4064
+ const dy = move.y !== void 0 ? move.y - m[5] : move.dy ?? 0;
4065
+ if (!Number.isFinite(dx) || !Number.isFinite(dy)) throw new LayerOperationError("x and y must be numbers.");
4066
+ requireUnlocked(layer, "position", "move it");
4067
+ const mx = Math.round(dx);
4068
+ const my = Math.round(dy);
4069
+ let moved = { ...transformTypeLayer(layer, [1, 0, 0, 1, mx, my]), surface: translateSurface(layer.surface, mx, my) };
4070
+ const mask = layer.mask;
4071
+ if (mask && mask.linked !== false && !mask.source && (mx || my)) moved = { ...moved, mask: { ...mask, surface: translateSurface(mask.surface, mx, my) } };
4072
+ this.commitLayers(mapLayer(this.state.layers, id, () => moved), void 0, commitToHistory);
4073
+ const t = moved.transform ?? [1, 0, 0, 1, 0, 0];
4074
+ return { offset: { x: Math.round(t[4]), y: Math.round(t[5]) }, bounds: this.getLayerBounds(id) };
4075
+ }
3821
4076
  const current = layer.type === "pixel" ? layer.offset : { x: 0, y: 0 };
3822
4077
  const x = move.x ?? current.x + (move.dx ?? 0);
3823
4078
  const y = move.y ?? current.y + (move.dy ?? 0);
@@ -3966,6 +4221,7 @@ var ImageEditorController = class {
3966
4221
  canvas: () => this.naturalSize,
3967
4222
  pixelsOf: (layer) => this.surfaceOf(layer),
3968
4223
  shapeEnv: () => this.shapeEnv(),
4224
+ textEnv: () => this.textEnv(),
3969
4225
  capture: () => this.captureHistoryEntry(),
3970
4226
  show: (layers, selection, active) => {
3971
4227
  this.state.layers = layers;
@@ -4066,7 +4322,7 @@ var ImageEditorController = class {
4066
4322
  if (turn !== 90 && turn !== -90 && turn !== 180) throw new LayerOperationError(`turn must be 90, -90 or 180, got ${String(turn)}.`);
4067
4323
  const session = this.beginFreeTransform({ interpolation: "nearest", ...init });
4068
4324
  try {
4069
- const b = session.smartObject ? transformedBox(session.startGeometry) ?? session.box : session.box;
4325
+ const b = session.smartObject || session.typeLayer ? transformedBox(session.startGeometry) ?? session.box : session.box;
4070
4326
  const cx = b.x + b.width / 2;
4071
4327
  const cy = b.y + b.height / 2;
4072
4328
  const ox = turn === 180 ? 0 : Math.round(cx - b.height / 2) - (cx - b.height / 2);
@@ -4786,6 +5042,7 @@ var ImageEditorController = class {
4786
5042
  } else {
4787
5043
  if (layer.type === "smart") throw new LayerOperationError(`Layer "${layer.name || id}" is a smart object; its pixels come from its contents. Rasterize it (rasterize_smart_object), edit its contents (edit_smart_object_contents), or ${action} on its mask or filter mask.`);
4788
5044
  if (layer.type === "shape") throw new LayerOperationError(`Layer "${layer.name || id}" is a shape layer; its pixels are drawn from its paths. Rasterize it first (rasterize_layer), or ${action} on its mask (channel mask).`);
5045
+ if (layer.type === "type") throw new LayerOperationError(`Layer "${layer.name || id}" is a type layer; its pixels are drawn from its text. Rasterize it first (rasterize_layer), or ${action} on its mask (channel mask).`);
4789
5046
  if (layer.type !== "pixel") throw new LayerOperationError(`Layer "${layer.name || id}" is a ${layer.type} layer; ${action} on a pixel layer, or on this layer's mask (channel mask).`);
4790
5047
  requireUnlocked(layer, "pixels", action);
4791
5048
  }
@@ -5491,7 +5748,11 @@ var ImageEditorController = class {
5491
5748
  // --- Annotations & Layers ---
5492
5749
  /** The layer showing annotation `id`: an annotation layer, or a shape layer migrated from it. */
5493
5750
  annotationLayer(id) {
5494
- return flattenLayers(this.state.layers).find((l) => (l.type === "annotation" || l.type === "shape") && annotationOfLayer(l)?.id === id);
5751
+ return flattenLayers(this.state.layers).find((l) => (l.type === "annotation" || l.type === "shape" || l.type === "type") && annotationOfLayer(l)?.id === id);
5752
+ }
5753
+ /** What migrating an annotation needs: the canvas size, patterns and the text shaper. */
5754
+ annotationEnv() {
5755
+ return { ...this.shapeEnv(), ...this.textEnv() };
5495
5756
  }
5496
5757
  /**
5497
5758
  * Adds an annotation as its own layer (top of the stack unless `aboveId` is given). Pen,
@@ -5503,7 +5764,7 @@ var ImageEditorController = class {
5503
5764
  this.recordHistory();
5504
5765
  const id = `ann_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
5505
5766
  const fullAnnotation = { ...annotation, id };
5506
- const layer = annotationLayerFor(fullAnnotation, this.shapeEnv());
5767
+ const layer = annotationLayerFor(fullAnnotation, this.annotationEnv());
5507
5768
  const loc = placement.aboveId ? findLayer(this.state.layers, placement.aboveId) : null;
5508
5769
  this.state.layers = loc ? insertLayer(this.state.layers, layer, loc.parent?.id ?? null, loc.index + 1) : insertLayer(this.state.layers, layer, null);
5509
5770
  this.state.selectedAnnotationId = id;
@@ -5523,24 +5784,14 @@ var ImageEditorController = class {
5523
5784
  }
5524
5785
  this.notify();
5525
5786
  }
5526
- /** `layer` (an annotation or migrated shape layer) showing `annotation` instead, keeping its other properties. */
5787
+ /** `layer` (an annotation, or a shape or type layer migrated from one) showing `annotation` instead, keeping its other properties. */
5527
5788
  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 };
5789
+ const fresh = annotationLayerFor(annotation, this.annotationEnv());
5790
+ const keep = { id: layer.id, visible: layer.visible, opacity: layer.opacity };
5791
+ for (const k of ["clipped", "mask", "locks", "effects"]) if (layer[k] !== void 0) keep[k] = layer[k];
5792
+ if (fresh.type !== "annotation" && layer.type !== "annotation" && layer.fillOpacity !== void 0) keep.fillOpacity = layer.fillOpacity;
5793
+ const blend = fresh.type === "annotation" ? layer.type === "annotation" ? layer.blend : "normal" : layer.type === "annotation" && layer.blend !== "normal" ? layer.blend : fresh.blend;
5794
+ return { ...fresh, ...keep, name, blend };
5544
5795
  }
5545
5796
  removeAnnotation(id) {
5546
5797
  this.recordHistory();
@@ -5555,7 +5806,7 @@ var ImageEditorController = class {
5555
5806
  clearAnnotations() {
5556
5807
  this.recordHistory();
5557
5808
  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);
5809
+ for (const l of flattenLayers(layers)) if (l.type === "annotation" || (l.type === "shape" || l.type === "type") && l.annotation) layers = mapLayer(layers, l.id, () => null);
5559
5810
  this.state.layers = layers;
5560
5811
  this.state.selectedAnnotationId = null;
5561
5812
  this.notify();
@@ -5794,6 +6045,233 @@ var ImageEditorController = class {
5794
6045
  this.commitLayers(setLayerMask(this.state.layers, target.id, { surface, defaultValue: 0 }));
5795
6046
  return target.id;
5796
6047
  }
6048
+ // --- Type layers ---
6049
+ textShaperValue;
6050
+ /** The shaper that lays out and draws type layers (`null` when there is none here). */
6051
+ getTextShaper() {
6052
+ if (this.textShaperValue === void 0) this.textShaperValue = createCanvasTextShaper({ createCanvas: (w, h) => this.renderer.createCanvas(w, h) });
6053
+ return this.textShaperValue;
6054
+ }
6055
+ /** Sets the text shaper (fonts) type layers lay out and draw with; existing layers redraw on their next edit. */
6056
+ setTextShaper(shaper) {
6057
+ this.textShaperValue = shaper;
6058
+ this.notify();
6059
+ }
6060
+ /** What type layers lay out and draw with. */
6061
+ textEnv() {
6062
+ return { ...this.naturalSize, shaper: this.getTextShaper(), createCanvas: (w, h) => this.renderer.createCanvas(w, h) };
6063
+ }
6064
+ /** Font families the shaper offers (generic families at least). */
6065
+ listFonts() {
6066
+ return this.getTextShaper()?.families?.() ?? ["sans-serif", "serif", "monospace"];
6067
+ }
6068
+ typeInfo(id) {
6069
+ const layer = requireTypeLayer(this.state.layers, id);
6070
+ const ranges = (runs) => {
6071
+ let at = 0;
6072
+ return runs.map((r) => {
6073
+ const out = { start: at, end: at + r.length, style: JSON.parse(JSON.stringify(r.style)) };
6074
+ at += r.length;
6075
+ return out;
6076
+ });
6077
+ };
6078
+ const shaper = this.getTextShaper();
6079
+ let lines = [];
6080
+ let missing = layer.missingFonts ?? [];
6081
+ if (shaper) {
6082
+ const layout = layoutText(layer, shaper);
6083
+ const r = (n) => Math.round(n * 1e3) / 1e3;
6084
+ lines = layout.lines.map((l) => ({ start: l.start, end: l.end, baseline: r(l.baseline), width: r(l.width) }));
6085
+ if (layer.rendering === "stored") missing = layout.missingFonts;
6086
+ }
6087
+ const info = {
6088
+ id: layer.id,
6089
+ name: layer.name,
6090
+ text: layer.text,
6091
+ runs: ranges(layer.runs),
6092
+ paragraphs: ranges(layer.paragraphs),
6093
+ frame: { ...layer.frame },
6094
+ transform: [...layer.transform ?? [1, 0, 0, 1, 0, 0]],
6095
+ lines,
6096
+ bounds: this.getLayerBounds(id),
6097
+ missingFonts: [...missing],
6098
+ rendering: layer.rendering === "stored" ? "stored" : "live"
6099
+ };
6100
+ if (layer.annotation) info.annotationId = layer.annotation.id;
6101
+ return info;
6102
+ }
6103
+ /** A type layer's text, runs, paragraphs, frame, lines and bounds. */
6104
+ getTextLayer(id) {
6105
+ return this.typeInfo(id);
6106
+ }
6107
+ /**
6108
+ * Adds a type layer (one undo step) and makes it active: point text at `x`, `y`, or paragraph
6109
+ * text wrapped in `box`. Refused without a text shaper. Returns its id.
6110
+ */
6111
+ addTextLayer(init) {
6112
+ const text = normalizeText(init.text);
6113
+ if (!Number.isFinite(init.x) || !Number.isFinite(init.y)) throw new LayerOperationError("x and y must be numbers (document pixels).");
6114
+ const style = patchTextStyle({ ...DEFAULT_TEXT_STYLE, color: this.state.paint.foreground }, init.style ?? {});
6115
+ const paragraph = patchParagraphStyle({}, init.paragraph ?? {});
6116
+ let frame = { kind: "point" };
6117
+ if (init.box) {
6118
+ const { width, height } = init.box;
6119
+ if (!(Number.isFinite(width) && width >= 1 && Number.isFinite(height) && height >= 1)) throw new LayerOperationError("box.width and box.height must be at least 1 pixel.");
6120
+ frame = { kind: "box", width, height };
6121
+ }
6122
+ if (init.rotation !== void 0 && !Number.isFinite(init.rotation)) throw new LayerOperationError("rotation must be a number of degrees.");
6123
+ if (init.opacity !== void 0 && !(Number.isFinite(init.opacity) && init.opacity >= 0 && init.opacity <= 1)) throw new LayerOperationError("opacity must be between 0 and 1.");
6124
+ if (init.blend !== void 0 && !isBlendMode(init.blend)) throw new LayerOperationError(`Unknown blend mode "${String(init.blend)}".`);
6125
+ const t = [1, 0, 0, 1, init.x, init.y];
6126
+ const transform = init.rotation ? multiplyMatrices(t, rotateMatrix(init.rotation, { x: init.x, y: init.y })) : t;
6127
+ const layer = createTypeLayer({ text, runs: [{ length: text.length, style }], paragraphs: [{ length: text.length, style: paragraph }], frame, transform, name: init.name ?? typeLayerName(text) }, this.textEnv());
6128
+ if (init.opacity !== void 0) layer.opacity = init.opacity;
6129
+ if (init.blend !== void 0) layer.blend = init.blend;
6130
+ const next = placeLayer(this.state.layers, layer, this.newLayerPlacement(init));
6131
+ this.commitLayers(next, () => {
6132
+ this.state.activeLayerId = layer.id;
6133
+ this.state.activeTarget = "pixels";
6134
+ });
6135
+ return layer.id;
6136
+ }
6137
+ /**
6138
+ * Replaces text [start, end) (default all of it) with `text`; inserted text takes the style at
6139
+ * `start`. One undo step unless `commitToHistory` is false (typing). Returns the layer.
6140
+ */
6141
+ setText(id, text, range = {}, commitToHistory = true) {
6142
+ const layer = requireTypeLayer(this.state.layers, id);
6143
+ const insert = normalizeText(text);
6144
+ const [start, end] = textRange(layer.text, range.start, range.end);
6145
+ const { layers } = updateTypeLayer(this.state.layers, id, (l) => replaceTextRange(l, start, end, insert), this.textEnv(), "edit its text");
6146
+ this.commitLayers(layers, void 0, commitToHistory);
6147
+ return this.typeInfo(id);
6148
+ }
6149
+ /** Sets character style fields over [start, end) (default all), one undo step. Returns the layer. */
6150
+ setTextStyle(id, style, range = {}, commitToHistory = true) {
6151
+ const layer = requireTypeLayer(this.state.layers, id);
6152
+ const [start, end] = textRange(layer.text, range.start, range.end);
6153
+ if (!Object.keys(style).length) throw new LayerOperationError("Give at least one style field to set.");
6154
+ patchTextStyle(DEFAULT_TEXT_STYLE, style);
6155
+ const { layers } = updateTypeLayer(
6156
+ this.state.layers,
6157
+ id,
6158
+ (l) => {
6159
+ if (start === end) {
6160
+ if (l.text.length) throw new LayerOperationError("The range is empty; give start < end (or leave both out for the whole text).");
6161
+ return { runs: [{ length: 0, style: patchTextStyle(styleAt(l.runs, 0, DEFAULT_TEXT_STYLE), style) }] };
6162
+ }
6163
+ return { runs: restyleRange(l.runs, start, end, (s) => patchTextStyle(s, style)) };
6164
+ },
6165
+ this.textEnv(),
6166
+ "restyle its text"
6167
+ );
6168
+ this.commitLayers(layers, void 0, commitToHistory);
6169
+ return this.typeInfo(id);
6170
+ }
6171
+ /** Sets paragraph style fields on every paragraph that [start, end) touches (default all), one undo step. */
6172
+ setParagraphStyle(id, style, range = {}, commitToHistory = true) {
6173
+ const layer = requireTypeLayer(this.state.layers, id);
6174
+ const [start, end] = textRange(layer.text, range.start, range.end);
6175
+ if (!Object.keys(style).length) throw new LayerOperationError("Give at least one paragraph field to set.");
6176
+ patchParagraphStyle({}, style);
6177
+ const { layers } = updateTypeLayer(
6178
+ this.state.layers,
6179
+ id,
6180
+ (l) => {
6181
+ let at = 0;
6182
+ const paragraphs = l.paragraphs.map((p) => {
6183
+ const a = at;
6184
+ at += p.length;
6185
+ const touches = a < end && at > start || start === end && start >= a && (start < at || at === l.text.length);
6186
+ return touches || !l.text.length ? { length: p.length, style: patchParagraphStyle(p.style, style) } : p;
6187
+ });
6188
+ return { paragraphs };
6189
+ },
6190
+ this.textEnv(),
6191
+ "restyle its paragraphs"
6192
+ );
6193
+ this.commitLayers(layers, void 0, commitToHistory);
6194
+ return this.typeInfo(id);
6195
+ }
6196
+ /**
6197
+ * Point text ↔ paragraph text: `{ kind: 'box', width, height }` wraps the text in a box (its
6198
+ * top-left where the first line's top-left is now), `{ kind: 'point' }` sets it on one line per
6199
+ * paragraph. Also resizes a box. One undo step.
6200
+ */
6201
+ setTextFrame(id, frame, commitToHistory = true) {
6202
+ const layer = requireTypeLayer(this.state.layers, id);
6203
+ if (frame.kind === "box" && !(Number.isFinite(frame.width) && frame.width >= 1 && Number.isFinite(frame.height) && frame.height >= 1)) throw new LayerOperationError("width and height must be at least 1 pixel.");
6204
+ if (frame.kind !== "box" && frame.kind !== "point") throw new LayerOperationError("kind must be point or box.");
6205
+ const shaper = this.getTextShaper();
6206
+ const { layers } = updateTypeLayer(
6207
+ this.state.layers,
6208
+ id,
6209
+ (l) => {
6210
+ if (frame.kind === "box" && l.frame.kind === "box") return { frame: { kind: "box", width: frame.width, height: frame.height } };
6211
+ if (!shaper) throw new LayerOperationError("Changing between point and paragraph text needs a text shaper to measure the text.");
6212
+ const lines = layoutText(l, shaper).lines;
6213
+ const first = lines[0];
6214
+ const dx = frame.kind === "box" ? first ? first.x : 0 : 0;
6215
+ const dy = frame.kind === "box" ? first ? first.baseline - first.ascent : 0 : first ? first.baseline : 0;
6216
+ const m = textMatrix(l);
6217
+ const shifted = [m[0], m[1], m[2], m[3], m[0] * dx + m[2] * dy + m[4], m[1] * dx + m[3] * dy + m[5]];
6218
+ const nextFrame = frame.kind === "box" ? { kind: "box", width: frame.width, height: frame.height } : { kind: "point" };
6219
+ return { frame: nextFrame, transform: shifted };
6220
+ },
6221
+ this.textEnv(),
6222
+ "change its frame"
6223
+ );
6224
+ this.commitLayers(layers, void 0, commitToHistory);
6225
+ return this.typeInfo(id);
6226
+ }
6227
+ /**
6228
+ * Convert to Shape: the text's glyph outlines become a shape layer with the same id, name and
6229
+ * layer properties (needs a shaper that gives outlines). The fill is the first run's colour;
6230
+ * `warnings` says when other colours were dropped. One undo step.
6231
+ */
6232
+ convertTextToShape(id) {
6233
+ const layer = requireTypeLayer(this.state.layers, id);
6234
+ requireUnlocked(layer, "all", "convert it");
6235
+ const shaper = this.getTextShaper();
6236
+ if (!shaper) throw new LayerOperationError("Convert to Shape needs a text shaper that gives glyph outlines; none is set here.");
6237
+ const parts = textOutlines(layer, shaper);
6238
+ const subpaths = parts.flatMap((p) => bezierToSubpaths(p.path));
6239
+ if (!subpaths.length) throw new LayerOperationError("The text draws nothing (it is empty or only spaces), so there is no shape to make.");
6240
+ const warnings = [];
6241
+ const colors = new Set(layer.runs.filter((r) => r.length > 0).map((r) => `${r.style.color}/${r.style.opacity ?? 1}`));
6242
+ if (colors.size > 1) warnings.push("The text had several colours; the shape takes the first one.");
6243
+ const missing = layoutText(layer, shaper).missingFonts;
6244
+ if (missing.length) warnings.push(`Missing fonts were drawn with a fallback: ${missing.join(", ")}.`);
6245
+ const first = layer.runs.find((r) => r.length > 0)?.style ?? DEFAULT_TEXT_STYLE;
6246
+ const fill = first.opacity !== void 0 && first.opacity !== 1 ? { type: "color", color: first.color, opacity: first.opacity } : { type: "color", color: first.color };
6247
+ const shape = createShapeLayer({ components: [{ op: "combine", subpaths }], fill, stroke: null, name: layer.name }, this.shapeEnv(), layer.id);
6248
+ const keep = { visible: layer.visible, opacity: layer.opacity, blend: layer.blend };
6249
+ for (const k of ["clipped", "mask", "locks", "effects", "fillOpacity"]) if (layer[k] !== void 0) keep[k] = layer[k];
6250
+ this.commitLayers(mapLayer(this.state.layers, id, () => ({ ...shape, ...keep })));
6251
+ return { layerId: id, warnings };
6252
+ }
6253
+ /** The layout of a type layer (lines, characters, bounds in text space), or `null` without a shaper. */
6254
+ getTextLayout(id) {
6255
+ const layer = requireTypeLayer(this.state.layers, id);
6256
+ const shaper = this.getTextShaper();
6257
+ return shaper ? layoutText(layer, shaper) : null;
6258
+ }
6259
+ /** The text index nearest a document point (for clicks on a type layer). */
6260
+ textIndexAt(id, point) {
6261
+ const layer = requireTypeLayer(this.state.layers, id);
6262
+ const layout = this.getTextLayout(id);
6263
+ return layout ? textIndexAt(layout, toTextSpace(layer, point)) : layer.text.length;
6264
+ }
6265
+ /** The caret at text index `index` as a document-space segment (top to bottom), for drawing it. */
6266
+ textCaret(id, index) {
6267
+ const layer = requireTypeLayer(this.state.layers, id);
6268
+ const layout = this.getTextLayout(id);
6269
+ if (!layout) return null;
6270
+ const c = caretAt(layout, Math.max(0, Math.min(layer.text.length, index)));
6271
+ const m = textMatrix(layer);
6272
+ const at = (x, y) => ({ x: m[0] * x + m[2] * y + m[4], y: m[1] * x + m[3] * y + m[5] });
6273
+ return { top: at(c.x, c.top), bottom: at(c.x, c.bottom), line: c.line };
6274
+ }
5797
6275
  // --- Shape layers ---
5798
6276
  /** What shape rendering needs: the canvas size and the document's patterns. */
5799
6277
  shapeEnv() {
@@ -5970,11 +6448,11 @@ var ImageEditorController = class {
5970
6448
  }
5971
6449
  } else if (layer.type === "smart") {
5972
6450
  return this.rasterizeSmartObject(id);
5973
- } else if (layer.type === "shape") {
5974
- next = shapeAsPixelLayer(layer);
6451
+ } else if (layer.type === "shape" || layer.type === "type") {
6452
+ next = layer.type === "shape" ? shapeAsPixelLayer(layer) : typeAsPixelLayer(layer);
5975
6453
  if (this.state.selectedAnnotationId && this.state.selectedAnnotationId === layer.annotation?.id) this.state.selectedAnnotationId = null;
5976
6454
  } 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.`);
6455
+ throw new LayerOperationError(`Only shape, type and annotation layers, smart objects and pixel layers with filters rasterize; "${layer.name || id}" is a ${layer.type} layer. Merge it down or flatten instead.`);
5978
6456
  }
5979
6457
  this.commitLayers(mapLayer(this.state.layers, id, () => next));
5980
6458
  return id;
@@ -6099,9 +6577,9 @@ var ImageEditorController = class {
6099
6577
  if (mask2) out2.mask = mask2;
6100
6578
  return out2;
6101
6579
  }
6102
- if (layer.type === "shape") {
6103
- add2("Shape layers were written as pixel layers", layer.name);
6104
- const out2 = shapeAsPixelLayer(layer);
6580
+ if (layer.type === "shape" || layer.type === "type") {
6581
+ add2(layer.type === "shape" ? "Shape layers were written as pixel layers" : "Type layers were written as pixel layers", layer.name);
6582
+ const out2 = layer.type === "shape" ? shapeAsPixelLayer(layer) : typeAsPixelLayer(layer);
6105
6583
  const mask2 = bakeMask(layer);
6106
6584
  if (mask2) out2.mask = mask2;
6107
6585
  else delete out2.mask;
@@ -6203,7 +6681,7 @@ var ImageEditorController = class {
6203
6681
  const bg = developTarget(layers, null);
6204
6682
  if (bg && bg.source !== "document") layers = mapLayer(layers, bg.id, () => ({ ...bg, source: "document" }));
6205
6683
  if (clearAnnotations) {
6206
- for (const l of flattenLayers(layers)) if (l.type === "annotation" || l.type === "shape" && l.annotation) layers = mapLayer(layers, l.id, () => null);
6684
+ for (const l of flattenLayers(layers)) if (l.type === "annotation" || (l.type === "shape" || l.type === "type") && l.annotation) layers = mapLayer(layers, l.id, () => null);
6207
6685
  this.state.selectedAnnotationId = null;
6208
6686
  }
6209
6687
  this.state.layers = layers;
@@ -6279,7 +6757,8 @@ var ImageEditorController = class {
6279
6757
  problems,
6280
6758
  smartContents: Array.isArray(data.smartContents) ? data.smartContents : [],
6281
6759
  canvas,
6282
- pattern: (pid) => storedPatterns.find((p) => p.id === pid) ?? null
6760
+ pattern: (pid) => storedPatterns.find((p) => p.id === pid) ?? null,
6761
+ text: this.textEnv()
6283
6762
  });
6284
6763
  this.lastLoadProblems = problems;
6285
6764
  s.crop = data.crop ? { ...data.crop } : null;
@@ -6482,7 +6961,7 @@ function collectStateTiles(layers, into) {
6482
6961
  for (const l of flattenLayers(layers)) {
6483
6962
  if (l.type === "pixel" && l.source !== "document") collectTiles(l.surface, into);
6484
6963
  if (l.mask && !l.mask.source) collectTiles(l.mask.surface, into);
6485
- if (l.type === "shape") collectTiles(l.surface, into);
6964
+ if (l.type === "shape" || l.type === "type") collectTiles(l.surface, into);
6486
6965
  if (l.type === "smart") {
6487
6966
  collectTiles(l.surface, into);
6488
6967
  if (l.smartFilters?.mask) collectTiles(l.smartFilters.mask.surface, into);
@@ -6531,7 +7010,7 @@ function resizePixels(px2, width, height) {
6531
7010
  function withoutAnnotations(layers) {
6532
7011
  const out = [];
6533
7012
  for (const l of layers) {
6534
- if (l.type === "annotation" || l.type === "shape" && l.annotation) continue;
7013
+ if (l.type === "annotation" || (l.type === "shape" || l.type === "type") && l.annotation) continue;
6535
7014
  out.push(l.type === "group" ? { ...l, children: withoutAnnotations(l.children) } : l);
6536
7015
  }
6537
7016
  return out;
@@ -6721,7 +7200,7 @@ function loadImageEditorDocument(input) {
6721
7200
  if (state.selection === void 0) delete state.selection;
6722
7201
  return { state, report: result.report, unknown: result.unknown };
6723
7202
  }
6724
- var LAYER_TYPES = /* @__PURE__ */ new Set(["pixel", "adjustment", "group", "annotation", "smart", "shape"]);
7203
+ var LAYER_TYPES = /* @__PURE__ */ new Set(["pixel", "adjustment", "group", "annotation", "smart", "shape", "type"]);
6725
7204
  function cleanLayers(value, dropped) {
6726
7205
  const out = [];
6727
7206
  for (const raw of value) {
@@ -7781,6 +8260,18 @@ function describeLayer(layer, activeId, boundsOf) {
7781
8260
  };
7782
8261
  if (layer.annotation) out.annotation = { id: layer.annotation.id, type: layer.annotation.type };
7783
8262
  break;
8263
+ case "type":
8264
+ out.bounds = boundsOf(layer);
8265
+ out.fillOpacity = round3(layer.fillOpacity ?? 1);
8266
+ out.text = {
8267
+ text: layer.text.length > 80 ? `${layer.text.slice(0, 80)}\u2026` : layer.text,
8268
+ frame: layer.frame.kind,
8269
+ runs: layer.runs.length,
8270
+ rendering: layer.rendering === "stored" ? "stored" : "live",
8271
+ missingFonts: layer.missingFonts ?? []
8272
+ };
8273
+ if (layer.annotation) out.annotation = { id: layer.annotation.id, type: layer.annotation.type };
8274
+ break;
7784
8275
  case "smart":
7785
8276
  out.bounds = boundsOf(layer);
7786
8277
  out.fillOpacity = round3(layer.fillOpacity ?? 1);
@@ -7826,7 +8317,7 @@ function createImageLayerTools(controller) {
7826
8317
  const listLayers = {
7827
8318
  name: "list_layers",
7828
8319
  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.",
8320
+ description: "Returns the layer stack bottom to top as a tree: id, type (pixel, adjustment, group, shape, type, smart, annotation), name, document bounds (pixel, shape, type and smart layers), visible, opacity, fillOpacity, blend, clipped, locks, a mask summary, an effects summary (master switch, scale, effect ids and kinds; details from list_layer_effects), filters (pixel layers), kind and params (adjustment layers), a shape summary (component operations and kinds, fill and stroke paint types; details from get_shape_layer), a text summary (the text, point or box frame, run count, live or stored pixels, missing fonts; details from get_text_layer), children (groups) and which layer is active. Also returns the active layer id and target (pixels or mask) and the canvas size.",
7830
8321
  inputSchema: { type: "object", properties: {}, additionalProperties: false },
7831
8322
  annotations: { title: "List image layers", ...READ2 },
7832
8323
  handler: () => {
@@ -8170,7 +8661,7 @@ function createImageLayerTools(controller) {
8170
8661
  const rasterize = {
8171
8662
  name: "rasterize_layer",
8172
8663
  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.",
8664
+ description: "Rasterize Layer: turns a shape or type layer into a pixel layer holding what it shows now (same id, name, opacity, blend, fill, mask and effects; its paths are gone, so later transforms resample pixels), turns an annotation layer into a pixel layer (needs a browser), rasterizes a smart object, or bakes a pixel layer's filters (develop, blur) into its pixels. One undo step. Returns the layer.",
8174
8665
  inputSchema: { type: "object", properties: { layerId: LAYER_ID2 }, required: ["layerId"], additionalProperties: false },
8175
8666
  annotations: { title: "Rasterize a layer", ...DESTRUCTIVE },
8176
8667
  handler: async ({ layerId }) => ({ layer: describe(await controller.rasterizeLayer(exists(layerId))) })
@@ -10019,6 +10510,176 @@ function createImageShapeTools(controller) {
10019
10510
  return [get, add2, update, setComponent, addComponent, removeComponent, merge];
10020
10511
  }
10021
10512
 
10513
+ // src/image-editor/type-tools.ts
10514
+ var READ9 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
10515
+ var WRITE8 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
10516
+ var WRITE_IDEMPOTENT6 = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
10517
+ var DESTRUCTIVE7 = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
10518
+ var LAYER_ID6 = { type: "string", description: "Type layer id from list_layers (type type) or add_text_layer." };
10519
+ var START = { type: "integer", minimum: 0, description: "Start of the range in UTF-16 code units, as get_text_layer counts them (default 0)." };
10520
+ var END = { type: "integer", minimum: 0, description: "End of the range, exclusive (default the end of the text)." };
10521
+ var STYLE = {
10522
+ fontFamily: { type: "string", description: "Font family name (list_fonts shows what is available; a missing family falls back, with a warning)." },
10523
+ fontWeight: { type: "integer", minimum: 1, maximum: 1e3, description: "Weight 100..900 (400 regular, 700 bold)." },
10524
+ italic: { type: "boolean", description: "Italic." },
10525
+ fontSize: { type: "number", minimum: 0.5, maximum: 5e3, description: "Size in document pixels." },
10526
+ color: { type: "string", description: "Colour (#rrggbb, or a CSS colour; its alpha becomes opacity)." },
10527
+ opacity: { type: ["number", "null"], minimum: 0, maximum: 1, description: "Text opacity 0..1 (null resets to 1)." },
10528
+ tracking: { type: ["number", "null"], minimum: -1e3, maximum: 1e4, description: "Letter spacing in 1/1000 em (null resets to 0)." },
10529
+ leading: { type: ["number", "null"], minimum: 0, maximum: 1e4, description: "Baseline-to-baseline distance in pixels (null = auto, the paragraph autoLeading \xD7 size)." },
10530
+ baselineShift: { type: ["number", "null"], minimum: -5e3, maximum: 5e3, description: "Pixels; positive raises the text (null resets to 0)." },
10531
+ underline: { type: "boolean", description: "Underline." },
10532
+ strikethrough: { type: "boolean", description: "Strike through." }
10533
+ };
10534
+ var PARAGRAPH = {
10535
+ align: { type: "string", enum: ["left", "center", "right", "justify"], description: "Alignment (justify stretches paragraph-box lines but the last)." },
10536
+ autoLeading: { type: ["number", "null"], minimum: 0.1, maximum: 10, description: "Auto leading as a multiple of the font size (default 1.2)." },
10537
+ firstLineIndent: { type: ["number", "null"], description: "First-line indent in pixels." },
10538
+ startIndent: { type: ["number", "null"], description: "Left indent in pixels." },
10539
+ endIndent: { type: ["number", "null"], description: "Right indent in pixels." },
10540
+ spaceBefore: { type: ["number", "null"], description: "Space above the paragraph in pixels (not before the first)." },
10541
+ spaceAfter: { type: ["number", "null"], description: "Space below the paragraph in pixels." }
10542
+ };
10543
+ function pick(args, keys) {
10544
+ const out = {};
10545
+ for (const k of keys) if (args[k] !== void 0) out[k] = args[k];
10546
+ return out;
10547
+ }
10548
+ function createImageTypeTools(controller) {
10549
+ const typeId = (id) => {
10550
+ if (typeof id !== "string" || !id) throw new LayerOperationError("layerId is required (a type layer id from list_layers).");
10551
+ requireTypeLayer(controller.getLayers(), id);
10552
+ return id;
10553
+ };
10554
+ const layerSummary = (id) => {
10555
+ const s = controller.getState();
10556
+ const loc = findLayer(s.layers, id);
10557
+ return loc ? { ...describeLayer(loc.layer, s.activeLayerId, (x) => controller.getLayerBounds(x.id)), parentId: loc.parent?.id ?? null, index: loc.index } : null;
10558
+ };
10559
+ const range = (args) => ({ start: args.start, end: args.end });
10560
+ const listFonts = {
10561
+ name: "list_fonts",
10562
+ title: "List fonts",
10563
+ description: "Returns the font families type layers can use here (generic families such as sans-serif always work). Changes nothing.",
10564
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
10565
+ annotations: { title: "List fonts", ...READ9 },
10566
+ handler: () => ({ families: controller.listFonts(), shaper: controller.getTextShaper()?.id ?? null })
10567
+ };
10568
+ const get = {
10569
+ name: "get_text_layer",
10570
+ title: "Read a type layer",
10571
+ description: "Returns a type layer\u2019s text, its character runs and paragraphs as [start, end) ranges in UTF-16 code units with their styles, the frame (point text, or a paragraph box with its size), the text-to-document transform, the laid-out lines (range, baseline, width), the rendered bounds, missing fonts and whether the pixels are live or stored from a file. Call before editing text you did not write, to get the ranges. Changes nothing.",
10572
+ inputSchema: { type: "object", properties: { layerId: LAYER_ID6 }, required: ["layerId"], additionalProperties: false },
10573
+ annotations: { title: "Read a type layer", ...READ9 },
10574
+ handler: ({ layerId }) => controller.getTextLayer(typeId(layerId))
10575
+ };
10576
+ const add2 = {
10577
+ name: "add_text_layer",
10578
+ title: "Add a type layer",
10579
+ description: "Adds a live type layer and makes it active: point text whose origin (x, y) is the first line\u2019s baseline at its alignment edge, or paragraph text wrapped in box {width, height} with its top-left at (x, y). Line breaks in text start paragraphs. Style and paragraph settings apply to the whole text (change ranges later with set_text_style). Default style: 24 px sans-serif in the foreground colour. One undo step. Returns the layer and the text layer, including missingFonts.",
10580
+ inputSchema: {
10581
+ type: "object",
10582
+ properties: {
10583
+ text: { type: "string", description: "The text; \\n starts a new paragraph." },
10584
+ x: { type: "number", description: "Document x of the origin, pixels." },
10585
+ y: { type: "number", description: "Document y of the origin (point text: the first baseline), pixels." },
10586
+ box: {
10587
+ type: "object",
10588
+ description: "Make it paragraph (area) text wrapped in this box, pixels.",
10589
+ properties: { width: { type: "number", minimum: 1, description: "Box width, pixels." }, height: { type: "number", minimum: 1, description: "Box height, pixels." } },
10590
+ required: ["width", "height"],
10591
+ additionalProperties: false
10592
+ },
10593
+ style: { type: "object", description: "Character style.", properties: STYLE, additionalProperties: false },
10594
+ paragraph: { type: "object", description: "Paragraph style.", properties: PARAGRAPH, additionalProperties: false },
10595
+ rotation: { type: "number", description: "Degrees clockwise about the origin (default 0)." },
10596
+ name: { type: "string", description: "Layer name (default the start of the text)." },
10597
+ opacity: { type: "number", minimum: 0, maximum: 1, description: "Layer opacity 0..1 (default 1)." },
10598
+ blend: { type: "string", enum: BLEND_MODES.map((m) => m.id), description: "Blend mode (default normal)." },
10599
+ aboveId: { type: "string", description: "Put the layer directly above this layer (default above the active layer)." },
10600
+ belowId: { type: "string", description: "Put the layer directly below this layer." },
10601
+ parentId: { type: "string", description: "Put the layer inside this group." },
10602
+ index: { type: "integer", minimum: 0, description: "Position among the siblings, 0 = bottom." }
10603
+ },
10604
+ required: ["text", "x", "y"],
10605
+ additionalProperties: false
10606
+ },
10607
+ annotations: { title: "Add a type layer", ...WRITE8 },
10608
+ handler: (args) => {
10609
+ const init = pick(args, ["text", "x", "y", "box", "style", "paragraph", "rotation", "name", "opacity", "blend", "aboveId", "belowId", "parentId", "index"]);
10610
+ if (init.blend !== void 0) init.blend = init.blend;
10611
+ const id = controller.addTextLayer(init);
10612
+ return { layer: layerSummary(id), text: controller.getTextLayer(id) };
10613
+ }
10614
+ };
10615
+ const setText = {
10616
+ name: "set_text",
10617
+ title: "Edit text",
10618
+ description: "Replaces the text in [start, end) (default all of it) with text; inserted text takes the style of the character at start. Use start = end to insert. Returns the text layer with its new runs.",
10619
+ inputSchema: { type: "object", properties: { layerId: LAYER_ID6, text: { type: "string", description: "Replacement text (\\n starts a paragraph; empty deletes the range)." }, start: START, end: END }, required: ["layerId", "text"], additionalProperties: false },
10620
+ annotations: { title: "Edit text", ...WRITE8 },
10621
+ handler: (args) => controller.setText(typeId(args.layerId), args.text, range(args))
10622
+ };
10623
+ const setStyle = {
10624
+ name: "set_text_style",
10625
+ title: "Style a range of text",
10626
+ description: "Sets character style fields (font, weight, italic, size, colour, tracking, leading, baseline shift, underline, strike-through) over [start, end) of a type layer (default all of it); fields not given keep their values per run. Returns the text layer with its runs and missingFonts.",
10627
+ inputSchema: { type: "object", properties: { layerId: LAYER_ID6, start: START, end: END, ...STYLE }, required: ["layerId"], additionalProperties: false },
10628
+ annotations: { title: "Style a range of text", ...WRITE_IDEMPOTENT6 },
10629
+ handler: (args) => {
10630
+ const style = pick(args, Object.keys(STYLE));
10631
+ if (!Object.keys(style).length) throw new LayerOperationError(`Give at least one style field: ${Object.keys(STYLE).join(", ")}.`);
10632
+ return controller.setTextStyle(typeId(args.layerId), style, range(args));
10633
+ }
10634
+ };
10635
+ const setParagraph = {
10636
+ name: "set_paragraph_style",
10637
+ title: "Style paragraphs",
10638
+ description: "Sets paragraph fields (alignment, auto leading, indents, spacing) on every paragraph that [start, end) touches (default all). Returns the text layer.",
10639
+ inputSchema: { type: "object", properties: { layerId: LAYER_ID6, start: START, end: END, ...PARAGRAPH }, required: ["layerId"], additionalProperties: false },
10640
+ annotations: { title: "Style paragraphs", ...WRITE_IDEMPOTENT6 },
10641
+ handler: (args) => {
10642
+ const style = pick(args, Object.keys(PARAGRAPH));
10643
+ if (!Object.keys(style).length) throw new LayerOperationError(`Give at least one paragraph field: ${Object.keys(PARAGRAPH).join(", ")}.`);
10644
+ return controller.setParagraphStyle(typeId(args.layerId), style, range(args));
10645
+ }
10646
+ };
10647
+ const setFrame = {
10648
+ name: "set_text_frame",
10649
+ title: "Point or paragraph text",
10650
+ description: "Converts between point text (kind point: one line per paragraph) and paragraph text (kind box: wrapped in width \xD7 height pixels, its top-left where the first line starts), or resizes a paragraph box. The first line stays in place. Returns the text layer with its new lines.",
10651
+ inputSchema: {
10652
+ type: "object",
10653
+ properties: {
10654
+ layerId: LAYER_ID6,
10655
+ kind: { type: "string", enum: ["point", "box"], description: "point or box." },
10656
+ width: { type: "number", minimum: 1, description: "Box width in pixels (kind box)." },
10657
+ height: { type: "number", minimum: 1, description: "Box height in pixels (kind box)." }
10658
+ },
10659
+ required: ["layerId", "kind"],
10660
+ additionalProperties: false
10661
+ },
10662
+ annotations: { title: "Point or paragraph text", ...WRITE_IDEMPOTENT6 },
10663
+ handler: ({ layerId, kind, width, height }) => {
10664
+ const id = typeId(layerId);
10665
+ if (kind === "box" && (width === void 0 || height === void 0)) throw new LayerOperationError("kind box needs width and height in pixels.");
10666
+ return controller.setTextFrame(id, kind === "box" ? { kind: "box", width, height } : { kind: "point" });
10667
+ }
10668
+ };
10669
+ const toShape = {
10670
+ name: "convert_text_to_shape",
10671
+ title: "Convert text to a shape",
10672
+ description: "Type \u203A Convert to Shape: the text\u2019s glyph outlines become a shape layer (same id, name, mask, effects, opacity, blend) that can be edited with the shape tools; the text is no longer editable. Needs a font shaper that gives outlines (the browser\u2019s canvas cannot). The fill is the first run\u2019s colour. One undo step. Returns the layer and warnings.",
10673
+ inputSchema: { type: "object", properties: { layerId: LAYER_ID6 }, required: ["layerId"], additionalProperties: false },
10674
+ annotations: { title: "Convert text to a shape", ...DESTRUCTIVE7 },
10675
+ handler: ({ layerId }) => {
10676
+ const { layerId: id, warnings } = controller.convertTextToShape(typeId(layerId));
10677
+ return { layer: layerSummary(id), shape: controller.getShapeLayer(id), warnings };
10678
+ }
10679
+ };
10680
+ return [listFonts, get, add2, setText, setStyle, setParagraph, setFrame, toShape];
10681
+ }
10682
+
10022
10683
  // src/image-editor/tools.ts
10023
10684
  var LAYER_PATH = /^layers\.([^.]+)\.(.+)$/;
10024
10685
  function adjustmentsSummary(adjustments) {
@@ -10259,6 +10920,7 @@ function createImageEditorToolProvider(controller, options = {}) {
10259
10920
  ...createImageTransformTools(controller),
10260
10921
  ...createImageSmartObjectTools(controller),
10261
10922
  ...createImageShapeTools(controller),
10923
+ ...createImageTypeTools(controller),
10262
10924
  ...createImageEffectTools(controller),
10263
10925
  ...createImagePaintTools(controller),
10264
10926
  ...createPigmentTools(),
@@ -10601,6 +11263,8 @@ function layerBadge(layer) {
10601
11263
  return "so";
10602
11264
  case "shape":
10603
11265
  return "shp";
11266
+ case "type":
11267
+ return "T";
10604
11268
  }
10605
11269
  }
10606
11270
  function dropZone(fraction, isGroup) {
@@ -10640,7 +11304,7 @@ function layerMenu(controller) {
10640
11304
  { action: "delete-mask", label: "Delete Layer Mask", enabled: Boolean(active?.mask) },
10641
11305
  { action: "toggle-mask", label: active?.mask?.enabled === false ? "Enable Layer Mask" : "Disable Layer Mask", enabled: Boolean(active?.mask) },
10642
11306
  { 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) },
11307
+ { action: "rasterize", label: "Rasterize Layer", enabled: active?.type === "annotation" || active?.type === "smart" || active?.type === "shape" || active?.type === "type" || isPixel && Boolean(active?.type === "pixel" && active.filters?.length) },
10644
11308
  { action: "convert-smart", label: "Convert to Smart Object", enabled: Boolean(active && active.type !== "annotation") },
10645
11309
  { action: "edit-contents", label: "Edit Contents", enabled: active?.type === "smart" },
10646
11310
  { action: "smart-via-copy", label: "New Smart Object via Copy", enabled: active?.type === "smart" },
@@ -11573,6 +12237,7 @@ export {
11573
12237
  D50,
11574
12238
  D65,
11575
12239
  DEFAULT_ADJUSTMENTS,
12240
+ DEFAULT_AUTO_LEADING,
11576
12241
  DEFAULT_BLENDER,
11577
12242
  DEFAULT_BRUSH,
11578
12243
  DEFAULT_DEPTH_MASK,
@@ -11582,6 +12247,7 @@ export {
11582
12247
  DEFAULT_REFINE_EDGE_PARAMS,
11583
12248
  DEFAULT_SELECTION_TOOL_OPTIONS,
11584
12249
  DEFAULT_SMUDGE,
12250
+ DEFAULT_TEXT_STYLE,
11585
12251
  DEFAULT_TRANSFORM,
11586
12252
  DEPTH_MASK_CONTROLS,
11587
12253
  DEVELOP_FILTER_ID,
@@ -11631,6 +12297,7 @@ export {
11631
12297
  MixerStroke,
11632
12298
  PAINTERLY_BLEND_MODES,
11633
12299
  PAINT_TOOLS,
12300
+ PARAGRAPH_STYLE_FIELDS,
11634
12301
  PATTERN_BLOCK_KEYS,
11635
12302
  PIGMENT_BANDS,
11636
12303
  PIGMENT_LAMBDA_MIN,
@@ -11658,6 +12325,9 @@ export {
11658
12325
  SmudgeTip,
11659
12326
  StrokeCoverage,
11660
12327
  SurfaceDraft,
12328
+ TEXT_ALIGNS,
12329
+ TEXT_RENDER_MARGIN,
12330
+ TEXT_STYLE_FIELDS,
11661
12331
  TILE,
11662
12332
  TILE_SHIFT,
11663
12333
  TINT_SCALE,
@@ -11691,6 +12361,7 @@ export {
11691
12361
  annotationLayerName,
11692
12362
  annotationOfLayer,
11693
12363
  annotationOfShape,
12364
+ annotationOfType,
11694
12365
  applyAdjustmentLayers,
11695
12366
  applyAdjustments,
11696
12367
  applyBoxBlur,
@@ -11733,6 +12404,7 @@ export {
11733
12404
  calculateCropDrag,
11734
12405
  canvasCompositeOperation,
11735
12406
  canvasToImagePoint,
12407
+ caretAt,
11736
12408
  changedTiles,
11737
12409
  clampControlValue,
11738
12410
  clampCropToImage,
@@ -11760,6 +12432,7 @@ export {
11760
12432
  coveringRect,
11761
12433
  createAdjustmentLayer,
11762
12434
  createBlenderScratch,
12435
+ createCanvasTextShaper,
11763
12436
  createImageCaptureSurface,
11764
12437
  createImageEditTools,
11765
12438
  createImageEditorToolProvider,
@@ -11771,10 +12444,15 @@ export {
11771
12444
  createImageShapeTools,
11772
12445
  createImageSmartObjectTools,
11773
12446
  createImageTransformTools,
12447
+ createImageTypeTools,
12448
+ createOutlineTextShaper,
11774
12449
  createPigment,
11775
12450
  createPigmentTools,
11776
12451
  createSelectionMask,
11777
12452
  createShapeLayer,
12453
+ createTypeLayer,
12454
+ cssFont,
12455
+ cssFontFamily,
11778
12456
  curveLut,
11779
12457
  curveLutFromLevels,
11780
12458
  curvePointToBox,
@@ -11843,9 +12521,12 @@ export {
11843
12521
  findLayer,
11844
12522
  findLayerControl,
11845
12523
  finishTile,
12524
+ fitParagraphRuns,
12525
+ fitRuns,
11846
12526
  flattenLayers,
11847
12527
  flattenPath,
11848
12528
  flipCorners,
12529
+ fontOf,
11849
12530
  formatControlValue,
11850
12531
  fullSelection,
11851
12532
  gaussianBlur,
@@ -11936,6 +12617,7 @@ export {
11936
12617
  layerTree,
11937
12618
  layerTypeLabel,
11938
12619
  layersFromLegacy,
12620
+ layoutText,
11939
12621
  legacyAdjustmentLayer,
11940
12622
  legacyMaskOf,
11941
12623
  legacyView,
@@ -11995,11 +12677,13 @@ export {
11995
12677
  normalizeSmudge,
11996
12678
  normalizeStoredAdjustments,
11997
12679
  normalizeSubpaths,
12680
+ normalizeText,
11998
12681
  offsetLayer,
11999
12682
  offsetShape,
12000
12683
  offsetSmartObject,
12001
12684
  openPsdFile,
12002
12685
  packRgba,
12686
+ paragraphRanges,
12003
12687
  parseAdjustmentBlock,
12004
12688
  parseBlend,
12005
12689
  parseColor,
@@ -12011,6 +12695,8 @@ export {
12011
12695
  parsePatternBlock,
12012
12696
  parseSelectionMode,
12013
12697
  parseVersionedDescriptor,
12698
+ patchParagraphStyle,
12699
+ patchTextStyle,
12014
12700
  photoFilterMatrix,
12015
12701
  pigment8,
12016
12702
  pixelLayerBounds,
@@ -12066,19 +12752,24 @@ export {
12066
12752
  renderShape,
12067
12753
  renderShapeRect,
12068
12754
  renderSmartObject,
12755
+ renderText,
12069
12756
  replaceContents,
12757
+ replaceTextRange,
12070
12758
  requireLayer,
12071
12759
  requireOperation,
12072
12760
  requireShapeLayer,
12073
12761
  requireSmartObject,
12762
+ requireTypeLayer,
12074
12763
  requireUnlocked,
12075
12764
  rerender,
12076
12765
  rerenderShape,
12766
+ rerenderText,
12077
12767
  resizePixels,
12078
12768
  resizeSelection,
12079
12769
  resizeWarpMesh,
12080
12770
  resolveExternalAssets,
12081
12771
  resolveMosaicRegionBounds,
12772
+ restyleRange,
12082
12773
  rgbToGray,
12083
12774
  rgbToHsl,
12084
12775
  rgbToReflectance,
@@ -12125,16 +12816,22 @@ export {
12125
12816
  smoothCoverage,
12126
12817
  smoothPoints,
12127
12818
  solidSurface,
12819
+ spliceRuns,
12128
12820
  splitOverlayAnnotations,
12129
12821
  srgb8ToLab,
12130
12822
  srgbToLinear,
12131
12823
  standardBlendMode,
12824
+ styleAt,
12132
12825
  sumHistograms,
12133
12826
  surfaceBytes,
12134
12827
  surfaceFromPixels,
12135
12828
  surfacesEqual,
12136
12829
  tempTintToXy,
12137
12830
  temperatureToKelvin,
12831
+ textIndexAt,
12832
+ textMatrix,
12833
+ textOutlines,
12834
+ textRange,
12138
12835
  tileCoord,
12139
12836
  tileKey,
12140
12837
  tileRect,
@@ -12143,6 +12840,7 @@ export {
12143
12840
  tipReach,
12144
12841
  tipShape,
12145
12842
  toHex,
12843
+ toTextSpace,
12146
12844
  toneCurve,
12147
12845
  toneLuts,
12148
12846
  toneRange,
@@ -12150,10 +12848,15 @@ export {
12150
12848
  transformMap,
12151
12849
  transformShapeComponents,
12152
12850
  transformSurface,
12851
+ transformTypeLayer,
12153
12852
  transformedBounds,
12154
12853
  transformedBox,
12155
12854
  transformedPoint,
12156
12855
  translateSurface,
12856
+ typeAsPixelLayer,
12857
+ typeFrame,
12858
+ typeFromAnnotation,
12859
+ typeLayerName,
12157
12860
  ungroupLayer,
12158
12861
  uniformTile,
12159
12862
  unionRect,
@@ -12163,6 +12866,7 @@ export {
12163
12866
  updateLayerMaskProps,
12164
12867
  updateShapeLayer,
12165
12868
  updateSmartFilter,
12869
+ updateTypeLayer,
12166
12870
  uvToXy,
12167
12871
  validateEffectGradient,
12168
12872
  validateLayerEffect,