@pixerate/editor 0.112.0 → 0.113.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.
@@ -4,7 +4,37 @@ import {
4
4
  import {
5
5
  createCaptureTool
6
6
  } from "../chunk-REOJJIY3.js";
7
- import "../chunk-VKK5VS2T.js";
7
+ import {
8
+ DEFAULT_RETOUCH,
9
+ RETOUCH_SAMPLES,
10
+ RegionPixelSource,
11
+ RetouchDabStroke,
12
+ SPOT_HEAL_TYPES,
13
+ TONE_RANGES,
14
+ TilePixelSource,
15
+ applyRetouchToTile,
16
+ bestOffset,
17
+ dilateMask,
18
+ dodgeBurn,
19
+ dodgeBurnCurve,
20
+ healPixels,
21
+ luma,
22
+ maskBounds,
23
+ membraneFill,
24
+ normalizeRetouch,
25
+ perDabStrength,
26
+ readSourceRegion,
27
+ rgbaViewOfGrey,
28
+ saturationOf,
29
+ seamlessClone,
30
+ smudgeEffect,
31
+ solveMembrane,
32
+ sponge,
33
+ spongeEffect,
34
+ spotHealPixels,
35
+ spotHealWindow,
36
+ toneOf
37
+ } from "../chunk-LPYOIVLA.js";
8
38
  import {
9
39
  ADJUSTMENT_KEYS,
10
40
  ADJUSTMENT_KINDS,
@@ -2620,7 +2650,7 @@ function createOutlineTextShaper(fonts, options = {}) {
2620
2650
  return { face: e.face, family, key: family.toLowerCase(), weight, italic, postScript };
2621
2651
  });
2622
2652
  const fallbackKey = (options.fallback ?? entries[0].family).toLowerCase();
2623
- const pick2 = (font) => {
2653
+ const pick3 = (font) => {
2624
2654
  if (font.postScriptName) {
2625
2655
  const exact = entries.find((e) => e.postScript === font.postScriptName);
2626
2656
  if (exact) return { entry: exact, available: true };
@@ -2648,22 +2678,22 @@ function createOutlineTextShaper(fonts, options = {}) {
2648
2678
  return {
2649
2679
  id: options.id ?? `outline:${entries.map((e) => `${e.family}/${e.weight}${e.italic ? "i" : ""}`).join(",")}`,
2650
2680
  resolve(font) {
2651
- const { entry, available } = pick2(font);
2681
+ const { entry, available } = pick3(font);
2652
2682
  return { family: entry.family, available };
2653
2683
  },
2654
2684
  metrics(font) {
2655
- const { face } = pick2(font).entry;
2685
+ const { face } = pick3(font).entry;
2656
2686
  const k = font.size / face.unitsPerEm;
2657
2687
  return { ascent: face.ascender * k, descent: -face.descender * k };
2658
2688
  },
2659
2689
  measure(text, font) {
2660
- const { face } = pick2(font).entry;
2690
+ const { face } = pick3(font).entry;
2661
2691
  const out = [0];
2662
2692
  for (let i = 1; i <= text.length; i++) out.push(face.getAdvanceWidth(text.slice(0, i), font.size, { kerning: true }));
2663
2693
  return out;
2664
2694
  },
2665
2695
  outline(text, font, x, y) {
2666
- return toPath(pick2(font).entry.face.getPath(text, x, y, font.size, { kerning: true }).commands);
2696
+ return toPath(pick3(font).entry.face.getPath(text, x, y, font.size, { kerning: true }).commands);
2667
2697
  },
2668
2698
  families() {
2669
2699
  return [...new Set(entries.map((e) => e.family))];
@@ -3153,6 +3183,9 @@ var FreeTransformSession = class {
3153
3183
 
3154
3184
  // src/image-editor/controller.ts
3155
3185
  var IMAGE_EDITOR_SCHEMA_VERSION = LAYERED_SCHEMA_VERSION;
3186
+ var RETOUCH_TOOLS = ["clone", "heal", "spot-heal", "smudge", "dodge", "burn", "sponge"];
3187
+ var isRetouchTool = (tool) => RETOUCH_TOOLS.includes(tool);
3188
+ var STROKE_TOOLS = ["brush", "eraser", "mixer", ...RETOUCH_TOOLS];
3156
3189
  function paintCoverageInto(base, coverage, apply, rects) {
3157
3190
  const tiles = new Map(base.tiles);
3158
3191
  const done = /* @__PURE__ */ new Set();
@@ -3186,6 +3219,18 @@ var PaintStrokeSession = class {
3186
3219
  if (stroke instanceof StrokeCoverage) {
3187
3220
  this.stroke = stroke;
3188
3221
  this.mixer = null;
3222
+ } else if ("kind" in stroke && stroke.kind === "coverage") {
3223
+ this.stroke = stroke.coverage;
3224
+ this.mixer = null;
3225
+ this.retouch = stroke;
3226
+ } else if ("kind" in stroke) {
3227
+ this.stroke = null;
3228
+ this.retouch = stroke;
3229
+ if (this.base.channels === 1) {
3230
+ const view = rgbaViewOfGrey(this.mixerPixels());
3231
+ this.mixer = stroke.create(view.target);
3232
+ this.flushView = view.flush;
3233
+ } else this.mixer = stroke.create(this.mixerPixels());
3189
3234
  } else {
3190
3235
  this.stroke = null;
3191
3236
  this.mixer = stroke.create(this.mixerPixels());
@@ -3202,10 +3247,20 @@ var PaintStrokeSession = class {
3202
3247
  stroke;
3203
3248
  mixer;
3204
3249
  mixerInit = null;
3250
+ retouch = null;
3251
+ /** Writes a mask's RGBA view back (sequential retouching on a mask). */
3252
+ flushView = null;
3253
+ started = false;
3205
3254
  /** Mixer tiles written since the last update: they get new tile objects so the compositor sees them change. */
3206
3255
  fresh = /* @__PURE__ */ new Set();
3207
3256
  last = null;
3208
3257
  closed = false;
3258
+ /** The new tile of a coverage stroke over the pre-stroke tile `src`. */
3259
+ compositeTile(src, tx, ty, options = {}, paint) {
3260
+ const r = this.retouch;
3261
+ if (r && r.kind === "coverage") return applyRetouchToTile(this.stroke.coverage, src, this.base.channels, tx, ty, paint ?? r.paint(), r.apply, options);
3262
+ return applyStrokeToTile(this.stroke.coverage, src, this.base.channels, tx, ty, this.apply, options);
3263
+ }
3209
3264
  /** The live pixels a mixer stroke reads and writes; base tiles are copied on first write. */
3210
3265
  mixerPixels() {
3211
3266
  const a = this.apply;
@@ -3232,8 +3287,14 @@ var PaintStrokeSession = class {
3232
3287
  /** Adds input points (document pixels); the stack shows the stroke so far. */
3233
3288
  push(points) {
3234
3289
  if (this.closed || !points.length) return;
3290
+ if (!this.started) {
3291
+ this.started = true;
3292
+ if (this.retouch?.kind === "coverage") this.retouch.start(points[0]);
3293
+ }
3235
3294
  this.last = points[points.length - 1];
3236
- this.update(this.mixer ? this.mixer.push(points) : this.stroke.push(points));
3295
+ const rects = this.mixer ? this.mixer.push(points) : this.stroke.push(points);
3296
+ this.flushView?.();
3297
+ this.update(rects);
3237
3298
  }
3238
3299
  update(rects) {
3239
3300
  if (!rects.length) return;
@@ -3257,7 +3318,7 @@ var PaintStrokeSession = class {
3257
3318
  const src = this.base.tiles.get(key)?.data ?? defaultTile(this.base);
3258
3319
  const prev = this.touched.has(key) ? this.live.get(key)?.data : void 0;
3259
3320
  this.touched.add(key);
3260
- const data = applyStrokeToTile(stroke.coverage, src, ch, tx, ty, this.apply, prev ? { into: prev, region: r } : {});
3321
+ const data = this.compositeTile(src, tx, ty, prev ? { into: prev, region: r } : {});
3261
3322
  this.live.set(key, { data, uniform: null });
3262
3323
  }
3263
3324
  this.controller._strokeUpdate(this.target.id, this.target.channel, new TiledSurface(ch, new Map(this.live), this.base.defaultValue), rects);
@@ -3266,9 +3327,22 @@ var PaintStrokeSession = class {
3266
3327
  end() {
3267
3328
  if (this.closed) throw new LayerOperationError("This stroke has already ended.");
3268
3329
  const last = this.last ?? void 0;
3269
- this.update(this.mixer ? this.mixer.finish(last) : this.stroke.finish(last));
3330
+ const rects = this.mixer ? this.mixer.finish(last) : this.stroke.finish(last);
3331
+ this.flushView?.();
3332
+ this.update(rects);
3270
3333
  this.closed = true;
3271
3334
  const ch = this.base.channels;
3335
+ const r = this.retouch;
3336
+ if (r?.kind === "coverage" && r.finalPaint) {
3337
+ const final = r.finalPaint(this.stroke.coverage);
3338
+ if (final) {
3339
+ for (const key of this.touched) {
3340
+ const { tx, ty } = tileCoord(key);
3341
+ const src = this.base.tiles.get(key)?.data ?? defaultTile(this.base);
3342
+ this.live.set(key, { data: this.compositeTile(src, tx, ty, {}, final), uniform: null });
3343
+ }
3344
+ }
3345
+ }
3272
3346
  const tiles = new Map(this.base.tiles);
3273
3347
  let changed = false;
3274
3348
  for (const key of this.touched) {
@@ -3280,11 +3354,12 @@ var PaintStrokeSession = class {
3280
3354
  if (!sameTile(prev, tile)) changed = true;
3281
3355
  }
3282
3356
  const surface = changed ? new TiledSurface(ch, tiles, this.base.defaultValue) : null;
3283
- const mixerLoad = this.mixer ? this.mixerInit.end(this.mixer.load) : void 0;
3357
+ const mixerLoad = this.mixerInit ? this.mixerInit.end(this.mixer.load) : void 0;
3284
3358
  this.controller._strokeCommit(this.before, this.target.id, this.target.channel, surface);
3285
3359
  const b = this.mixer ? this.mixer.bounds : this.stroke.coverage.bounds;
3286
3360
  const result = { layerId: this.target.id, channel: this.target.channel, bounds: changed ? b : null, dabs: (this.mixer ?? this.stroke).dabCount };
3287
3361
  if (mixerLoad) result.mixerLoad = mixerLoad;
3362
+ if (r?.result) Object.assign(result, r.result());
3288
3363
  return result;
3289
3364
  }
3290
3365
  /** Abandons the stroke and restores the layer. */
@@ -3423,7 +3498,7 @@ var ImageEditorController = class {
3423
3498
  activeLayerId: null,
3424
3499
  activeTarget: "pixels",
3425
3500
  outputOpacity: 1,
3426
- paint: { brush: { ...DEFAULT_BRUSH }, foreground: "#000000", background: "#ffffff", mixer: { ...DEFAULT_MIXER }, mixerLoad: EMPTY_MIXER_LOAD },
3501
+ paint: { brush: { ...DEFAULT_BRUSH }, foreground: "#000000", background: "#ffffff", mixer: { ...DEFAULT_MIXER }, mixerLoad: EMPTY_MIXER_LOAD, retouch: { ...DEFAULT_RETOUCH }, cloneSource: null },
3427
3502
  fit: options.fit ?? "margin"
3428
3503
  };
3429
3504
  if (options.image instanceof Object && "width" in options.image) {
@@ -5018,12 +5093,16 @@ var ImageEditorController = class {
5018
5093
  /** Brush settings, colours and tool options of the paint tools (view state, no undo). */
5019
5094
  setPaintOptions(options) {
5020
5095
  const cur = this.state.paint;
5096
+ const retouch = options.retouch ? normalizeRetouch(options.retouch, cur.retouch) : cur.retouch;
5097
+ const cloneSource = cur.cloneSource && retouch.aligned !== cur.retouch.aligned ? { ...cur.cloneSource, offset: null } : cur.cloneSource;
5021
5098
  const next = {
5022
5099
  brush: options.brush ? normalizeBrush({ ...cur.brush, ...options.brush }) : cur.brush,
5023
5100
  foreground: options.foreground !== void 0 ? toHex(parseColor(options.foreground)) : cur.foreground,
5024
5101
  background: options.background !== void 0 ? toHex(parseColor(options.background)) : cur.background,
5025
5102
  mixer: options.mixer ? normalizeMixer({ ...cur.mixer, ...options.mixer }) : cur.mixer,
5026
- mixerLoad: cur.mixerLoad
5103
+ mixerLoad: cur.mixerLoad,
5104
+ retouch,
5105
+ cloneSource
5027
5106
  };
5028
5107
  this.state.paint = next;
5029
5108
  this.notify();
@@ -5053,6 +5132,17 @@ var ImageEditorController = class {
5053
5132
  this.notify();
5054
5133
  return EMPTY_MIXER_LOAD;
5055
5134
  }
5135
+ /**
5136
+ * Sets (Alt-click) or clears the Clone Stamp and Healing Brush source point, in document
5137
+ * pixels. The next stroke fixes the offset from its first point. View state, no undo.
5138
+ */
5139
+ setCloneSource(point) {
5140
+ if (point && (!Number.isFinite(point.x) || !Number.isFinite(point.y))) throw new LayerOperationError("The clone source needs finite x and y (document pixels).");
5141
+ const source = point ? { x: Math.round(point.x), y: Math.round(point.y), offset: null } : null;
5142
+ this.state.paint = { ...this.state.paint, cloneSource: source };
5143
+ this.notify();
5144
+ return source;
5145
+ }
5056
5146
  /** D: black foreground, white background. */
5057
5147
  resetPaintColors() {
5058
5148
  this.setPaintOptions({ foreground: "#000000", background: "#ffffff" });
@@ -5107,8 +5197,8 @@ var ImageEditorController = class {
5107
5197
  */
5108
5198
  beginStroke(init = {}) {
5109
5199
  const tool = init.tool ?? "brush";
5110
- if (tool !== "brush" && tool !== "eraser" && tool !== "mixer") throw new LayerOperationError(`Unknown paint tool "${String(tool)}"; use brush, eraser or mixer.`);
5111
- const target = this.paintTarget(init.layerId, init.channel, tool === "eraser" ? "erase" : "paint");
5200
+ if (!STROKE_TOOLS.includes(tool)) throw new LayerOperationError(`Unknown paint tool "${String(tool)}"; use one of ${STROKE_TOOLS.join(", ")}.`);
5201
+ const target = this.paintTarget(init.layerId, init.channel, tool === "eraser" ? "erase" : isRetouchTool(tool) ? "retouch" : "paint");
5112
5202
  if (tool === "mixer" && target.channel !== "pixels") {
5113
5203
  throw new LayerOperationError("The Mixer Brush paints colour on pixels, not on masks; target pixels (channel pixels), or paint the mask with the brush.");
5114
5204
  }
@@ -5130,8 +5220,101 @@ var ImageEditorController = class {
5130
5220
  offset: target.offset
5131
5221
  };
5132
5222
  if (tool === "mixer") return new PaintStrokeSession(this, target, this.mixerStroke(init, brush), apply);
5223
+ if (isRetouchTool(tool)) return new PaintStrokeSession(this, target, this.retouchStroke(tool, init, brush, target, apply), apply);
5133
5224
  return new PaintStrokeSession(this, target, new StrokeCoverage(brush), apply);
5134
5225
  }
5226
+ /** A retouching stroke's start: its settings, what it samples and how it composites. */
5227
+ retouchStroke(tool, init, brush, target, apply) {
5228
+ const settings = normalizeRetouch(init.retouch ?? {}, this.state.paint.retouch);
5229
+ const mask = target.channel !== "pixels";
5230
+ if (tool === "sponge" && mask) throw new LayerOperationError("The Sponge changes saturation, and a mask has none; target pixels (channel pixels), or use dodge or burn on the mask.");
5231
+ if (tool === "smudge") return { kind: "dabs", create: (pixels) => new RetouchDabStroke(brush, pixels, smudgeEffect(brush, settings.strength)) };
5232
+ if (tool === "sponge") return { kind: "dabs", create: (pixels) => new RetouchDabStroke(brush, pixels, spongeEffect(brush, settings.spongeMode === "saturate", settings.vibrance)) };
5233
+ const retouchApply = {
5234
+ opacity: tool === "clone" || tool === "heal" ? brush.opacity : 1,
5235
+ blend: tool === "clone" && !mask ? brush.blend : "normal",
5236
+ lockTransparency: target.lockTransparency,
5237
+ selection: apply.selection,
5238
+ offset: target.offset
5239
+ };
5240
+ const coverage = new StrokeCoverage(brush);
5241
+ if (tool === "dodge" || tool === "burn") {
5242
+ const paint2 = { kind: "tone", tone: toneOf(tool, settings) };
5243
+ return { kind: "coverage", coverage, apply: retouchApply, start: () => {
5244
+ }, paint: () => paint2 };
5245
+ }
5246
+ const sample = mask ? "current" : settings.sample;
5247
+ const own = this.surfaceSource(target.surface, target.offset);
5248
+ const source = sample === "current" ? own : this.compositeSource(sample === "all" ? null : target.id);
5249
+ const pad = (r, m) => ({ x: r.x - m, y: r.y - m, width: r.width + 2 * m, height: r.height + 2 * m });
5250
+ const covered = (cov, r) => {
5251
+ const m = new Uint8Array(r.width * r.height);
5252
+ for (let y = 0; y < r.height; y++) for (let x = 0; x < r.width; x++) m[y * r.width + x] = cov.get(r.x + x, r.y + y) > 0 ? 1 : 0;
5253
+ return m;
5254
+ };
5255
+ if (tool === "spot-heal") {
5256
+ const { width, height } = this.naturalSize;
5257
+ let patch = null;
5258
+ const trail = { kind: "color", color: [0, 0, 0, 96] };
5259
+ return {
5260
+ kind: "coverage",
5261
+ coverage,
5262
+ apply: retouchApply,
5263
+ start: () => {
5264
+ },
5265
+ paint: () => trail,
5266
+ finalPaint: (cov) => {
5267
+ if (!cov.bounds) return null;
5268
+ const win = spotHealWindow(cov.bounds, { width, height });
5269
+ if (win.width <= 0 || win.height <= 0) return null;
5270
+ const img = readSourceRegion(source, win);
5271
+ const hole = covered(cov, win);
5272
+ const healed = spotHealPixels(win.width, win.height, img, hole, brush.size, settings.spotType, (off) => {
5273
+ patch = off ? { x: off[0], y: off[1] } : null;
5274
+ });
5275
+ return { kind: "source", source: new RegionPixelSource(win, healed), dx: 0, dy: 0 };
5276
+ },
5277
+ result: () => ({ spotHeal: { offset: patch } })
5278
+ };
5279
+ }
5280
+ if (init.source) this.setCloneSource(init.source);
5281
+ const cs = this.state.paint.cloneSource;
5282
+ if (!cs) throw new LayerOperationError(`The ${tool === "clone" ? "Clone Stamp" : "Healing Brush"} needs a source point; Alt-click the canvas, call set_clone_source, or pass source { x, y }.`);
5283
+ let offset = { x: 0, y: 0 };
5284
+ let first = { x: 0, y: 0 };
5285
+ const paint = () => ({ kind: "source", source, dx: offset.x, dy: offset.y });
5286
+ return {
5287
+ kind: "coverage",
5288
+ coverage,
5289
+ apply: retouchApply,
5290
+ start: (p) => {
5291
+ first = { x: Math.round(p.x), y: Math.round(p.y) };
5292
+ offset = settings.aligned && cs.offset ? { ...cs.offset } : { x: cs.x - first.x, y: cs.y - first.y };
5293
+ if (settings.aligned) this.state.paint = { ...this.state.paint, cloneSource: { x: cs.x, y: cs.y, offset: { ...offset } } };
5294
+ },
5295
+ paint,
5296
+ finalPaint: tool === "heal" ? (cov) => {
5297
+ if (!cov.bounds) return null;
5298
+ const g = pad(cov.bounds, 2);
5299
+ const src = readSourceRegion(source, g, offset.x, offset.y);
5300
+ const dst = readSourceRegion(source, g);
5301
+ return { kind: "source", source: new RegionPixelSource(g, healPixels(g.width, g.height, src, dst, covered(cov, g))), dx: 0, dy: 0 };
5302
+ } : void 0,
5303
+ result: () => ({ clone: { offset: { ...offset }, source: { x: first.x + offset.x, y: first.y + offset.y }, aligned: settings.aligned } })
5304
+ };
5305
+ }
5306
+ /** A pixel source over a surface (layer pixels or a mask) in layer space at `offset`. */
5307
+ surfaceSource(surface, offset) {
5308
+ return new TilePixelSource((tx, ty) => surface.tiles.get(tileKey(tx, ty))?.data ?? (surface.defaultValue ? defaultTile(surface) : null), surface.channels, offset);
5309
+ }
5310
+ /** A pixel source over the composite of the current layers (through `throughId` and below, or all of them), as it is now. */
5311
+ compositeSource(throughId) {
5312
+ const stack = withoutAnnotations(this.state.layers);
5313
+ const layers = throughId ? layersThrough(stack, throughId) : stack;
5314
+ const compositor = new LayerCompositor();
5315
+ const sources = this.compositorSources();
5316
+ return new TilePixelSource((tx, ty) => compositor.tile(layers, tx, ty, sources), 4);
5317
+ }
5135
5318
  /** A Mixer Brush stroke's start: the settings, and the load (refilled when asked or set to). */
5136
5319
  mixerStroke(init, brush) {
5137
5320
  const settings = normalizeMixer({ ...this.state.paint.mixer, ...init.mixer ?? {} });
@@ -7033,6 +7216,27 @@ function collectStateTiles(layers, into) {
7033
7216
  }
7034
7217
  }
7035
7218
  }
7219
+ function layersThrough(layers, id) {
7220
+ const walk = (list) => {
7221
+ const out = [];
7222
+ for (const l of list) {
7223
+ if (l.id === id) {
7224
+ out.push(l);
7225
+ return { out, found: true };
7226
+ }
7227
+ if (l.type === "group") {
7228
+ const sub2 = walk(l.children);
7229
+ if (sub2.found) {
7230
+ out.push({ ...l, children: sub2.out });
7231
+ return { out, found: true };
7232
+ }
7233
+ }
7234
+ out.push(l);
7235
+ }
7236
+ return { out, found: false };
7237
+ };
7238
+ return walk(layers).out;
7239
+ }
7036
7240
  function pixelsOnly(layers) {
7037
7241
  const out = [];
7038
7242
  for (const l of layers) {
@@ -9175,9 +9379,73 @@ var PAINT_TOOLS = [
9175
9379
  { id: "move", label: "Move", shortcut: "V" },
9176
9380
  { id: "brush", label: "Brush", shortcut: "B" },
9177
9381
  { id: "mixer", label: "Mixer", shortcut: "Shift+B" },
9178
- { id: "eraser", label: "Eraser", shortcut: "E" }
9382
+ { id: "eraser", label: "Eraser", shortcut: "E" },
9383
+ { id: "clone", label: "Clone", shortcut: "S" },
9384
+ { id: "heal", label: "Heal", shortcut: "Shift+J" },
9385
+ { id: "spot-heal", label: "Spot Heal", shortcut: "J" },
9386
+ { id: "smudge", label: "Smudge", shortcut: "" },
9387
+ { id: "dodge", label: "Dodge", shortcut: "O" },
9388
+ { id: "burn", label: "Burn", shortcut: "Shift+O" },
9389
+ { id: "sponge", label: "Sponge", shortcut: "Shift+O" }
9390
+ ];
9391
+ var isPaintTool = (tool) => PAINT_TOOLS.some((t) => t.id === tool);
9392
+ var isRetouchPaintTool = (tool) => tool === "clone" || tool === "heal" || tool === "spot-heal" || tool === "smudge" || tool === "dodge" || tool === "burn" || tool === "sponge";
9393
+ var RETOUCH_OPTION_CONTROLS = [
9394
+ { id: "exposure", label: "Exposure", section: "layer", min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain2, displayScale: 100, unit: "%", tools: ["dodge", "burn"] },
9395
+ { id: "strength", label: "Strength", section: "layer", min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain2, displayScale: 100, unit: "%", tools: ["smudge"] }
9179
9396
  ];
9180
- var isPaintTool = (tool) => tool === "move" || tool === "brush" || tool === "mixer" || tool === "eraser";
9397
+ var RETOUCH_CHOICES = [
9398
+ {
9399
+ id: "sample",
9400
+ label: "Sample",
9401
+ tools: ["clone", "heal", "spot-heal"],
9402
+ options: [
9403
+ { id: "current", label: "Current", title: "Sample the layer being retouched" },
9404
+ { id: "current-below", label: "Current & Below", title: "Sample this layer and the layers under it" },
9405
+ { id: "all", label: "All", title: "Sample every layer (retouch onto an empty layer)" }
9406
+ ]
9407
+ },
9408
+ {
9409
+ id: "spotType",
9410
+ label: "Type",
9411
+ tools: ["spot-heal"],
9412
+ options: [
9413
+ { id: "proximity-match", label: "Proximity Match", title: "Copy the best matching patch nearby" },
9414
+ { id: "smooth", label: "Smooth", title: "Fill smoothly from the edges" }
9415
+ ]
9416
+ },
9417
+ {
9418
+ id: "range",
9419
+ label: "Range",
9420
+ tools: ["dodge", "burn"],
9421
+ options: [
9422
+ { id: "shadows", label: "Shadows", title: "Change the dark tones most" },
9423
+ { id: "midtones", label: "Midtones", title: "Change the middle tones most" },
9424
+ { id: "highlights", label: "Highlights", title: "Change the light tones most" }
9425
+ ]
9426
+ },
9427
+ {
9428
+ id: "spongeMode",
9429
+ label: "Mode",
9430
+ tools: ["sponge"],
9431
+ options: [
9432
+ { id: "desaturate", label: "Desaturate", title: "Take colour out" },
9433
+ { id: "saturate", label: "Saturate", title: "Add colour" }
9434
+ ]
9435
+ }
9436
+ ];
9437
+ var RETOUCH_TOGGLES = [
9438
+ { id: "aligned", label: "Aligned", tools: ["clone", "heal"] },
9439
+ { id: "protectTones", label: "Protect tones", tools: ["dodge", "burn"] },
9440
+ { id: "vibrance", label: "Vibrance", tools: ["sponge"] }
9441
+ ];
9442
+ function setRetouchChoice(controller, id, value) {
9443
+ controller.setPaintOptions({ retouch: { [id]: value } });
9444
+ }
9445
+ function cloneSourceLabel(controller) {
9446
+ const cs = controller.getState().paint.cloneSource;
9447
+ return cs ? `Source ${cs.x}, ${cs.y}${cs.offset ? ` (offset ${cs.offset.x}, ${cs.offset.y})` : ""}` : "Alt-click to set the source";
9448
+ }
9181
9449
  function rgbUnitToHex(c) {
9182
9450
  return `#${c.map((v) => Math.round(Math.min(1, Math.max(0, v)) * 255).toString(16).padStart(2, "0")).join("")}`;
9183
9451
  }
@@ -9203,6 +9471,18 @@ function handlePaintShortcut(controller, e) {
9203
9471
  case "v":
9204
9472
  controller.setTool("move");
9205
9473
  return true;
9474
+ case "s":
9475
+ controller.setTool("clone");
9476
+ return true;
9477
+ case "j":
9478
+ controller.setTool(e.shiftKey || key === "J" ? controller.getState().activeTool === "heal" ? "spot-heal" : "heal" : "spot-heal");
9479
+ return true;
9480
+ case "o": {
9481
+ const cur = controller.getState().activeTool;
9482
+ const next = e.shiftKey || key === "O" ? cur === "dodge" ? "burn" : cur === "burn" ? "sponge" : "dodge" : "dodge";
9483
+ controller.setTool(next);
9484
+ return true;
9485
+ }
9206
9486
  case "d":
9207
9487
  controller.resetPaintColors();
9208
9488
  return true;
@@ -9258,9 +9538,13 @@ var PaintGesture = class {
9258
9538
  this.move = { id: layer.id, start: { x: sample.x, y: sample.y }, origin, moved: false };
9259
9539
  return null;
9260
9540
  }
9261
- if (tool === "brush" || tool === "eraser" || tool === "mixer") {
9541
+ if ((tool === "clone" || tool === "heal") && sample.altKey) {
9542
+ this.controller.setCloneSource({ x: sample.x, y: sample.y });
9543
+ return null;
9544
+ }
9545
+ if (isPaintTool(tool)) {
9262
9546
  this.stroke = this.controller.beginStroke({ tool });
9263
- this.stroke.push([sample]);
9547
+ this.stroke.push([{ x: sample.x, y: sample.y, pressure: sample.pressure }]);
9264
9548
  return null;
9265
9549
  }
9266
9550
  } catch (err) {
@@ -9491,8 +9775,272 @@ function createImagePaintTools(controller) {
9491
9775
  return [paintStroke, fill, clear, getMixer, loadMixerTool, cleanMixerTool];
9492
9776
  }
9493
9777
 
9494
- // src/image-editor/psd-tools.ts
9778
+ // src/image-editor/retouch-tools.ts
9495
9779
  var READ5 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
9780
+ var WRITE5 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
9781
+ var POINTS = {
9782
+ type: "array",
9783
+ minItems: 1,
9784
+ maxItems: 1e4,
9785
+ description: "The stroke path in document pixels; one point is one dab.",
9786
+ items: {
9787
+ type: "object",
9788
+ properties: {
9789
+ x: { type: "number", description: "Document x in px." },
9790
+ y: { type: "number", description: "Document y in px." },
9791
+ pressure: { type: "number", minimum: 0, maximum: 1, description: "Pen pressure (default 1)." }
9792
+ },
9793
+ required: ["x", "y"],
9794
+ additionalProperties: false
9795
+ }
9796
+ };
9797
+ var BRUSH2 = {
9798
+ type: "object",
9799
+ description: "Brush tip; missing fields use the current paint options (default round, hard, 20 px).",
9800
+ properties: {
9801
+ size: { type: "number", minimum: 1, maximum: MAX_BRUSH_SIZE, description: "Diameter in px (default 20)." },
9802
+ hardness: { type: "number", minimum: 0, maximum: 1, description: "Edge hardness 0 (soft) .. 1 (hard, default). Retouching usually wants a soft tip (0..0.5)." },
9803
+ spacing: { type: "number", minimum: 0.01, maximum: 10, description: "Dab spacing as a fraction of the diameter (default 0.1)." },
9804
+ opacity: { type: "number", minimum: 0, maximum: 1, description: "Stroke opacity (Clone Stamp and Healing Brush only; default 1)." },
9805
+ flow: { type: "number", minimum: 0, maximum: 1, description: "Build-up per dab (default 1). For the Sponge, how strongly each pass moves saturation." },
9806
+ smoothing: { type: "number", minimum: 0, maximum: 0.95, description: "Point smoothing 0..0.95 (default 0)." },
9807
+ pressureSize: { type: "boolean", description: "Point pressure scales the size (default true)." },
9808
+ pressureOpacity: { type: "boolean", description: "Point pressure scales the opacity (default false)." }
9809
+ },
9810
+ additionalProperties: false
9811
+ };
9812
+ var TARGET2 = {
9813
+ layerId: { type: "string", description: "Layer to retouch (default the active layer; see list_layers)." },
9814
+ channel: {
9815
+ type: "string",
9816
+ enum: ["pixels", "mask", "filter-mask"],
9817
+ description: "pixels (default for a pixel layer), the layer's mask, or a smart object's filter mask. Masks are grey values and sample only themselves."
9818
+ }
9819
+ };
9820
+ var SAMPLE = {
9821
+ type: "string",
9822
+ enum: [...RETOUCH_SAMPLES],
9823
+ description: "What is sampled: current (default: the target layer), current-below (the composite of this layer and everything under it), or all (the composite of every layer). Sample current-below or all to retouch onto an empty layer above the image."
9824
+ };
9825
+ function readTarget2(args) {
9826
+ return { layerId: args.layerId, channel: args.channel };
9827
+ }
9828
+ function checkSource(controller, source) {
9829
+ if (source === void 0) return void 0;
9830
+ const p = source;
9831
+ const { width, height } = controller.getState().imageDimensions;
9832
+ if (!(p.x >= 0 && p.y >= 0 && p.x < width && p.y < height)) {
9833
+ throw new LayerOperationError(`source (${p.x}, ${p.y}) is outside the ${width} \xD7 ${height} canvas; pick a point on the image to sample.`);
9834
+ }
9835
+ return p;
9836
+ }
9837
+ var SOURCE = {
9838
+ type: "object",
9839
+ description: "Source point in document pixels (an Alt-click). Given, it replaces the clone source and the offset starts afresh at this stroke; missing, the current clone source is used (set_clone_source).",
9840
+ properties: { x: { type: "number", description: "Source x in px." }, y: { type: "number", description: "Source y in px." } },
9841
+ required: ["x", "y"],
9842
+ additionalProperties: false
9843
+ };
9844
+ function createImageRetouchTools(controller) {
9845
+ const stroke = (tool, args, retouch) => controller.paintStroke({
9846
+ tool,
9847
+ points: args.points,
9848
+ brush: args.brush,
9849
+ retouch,
9850
+ source: checkSource(controller, args.source),
9851
+ ...readTarget2(args)
9852
+ });
9853
+ const cloneStamp = {
9854
+ name: "clone_stamp",
9855
+ title: "Clone Stamp stroke",
9856
+ description: "Copies pixels from the clone source along a stroke (Photoshop's Clone Stamp): each point paints what lies at point + offset, where the offset is source \u2212 the stroke's first point. With aligned on, later strokes keep that offset; off, every stroke starts again at the source point. Samples the layer, the layers up to it, or all layers, from before the stroke. The selection limits it; a transparency-locked layer keeps its alpha. One undo step. Returns the layerId, channel, changed bounds, dab count and clone {offset, source, aligned}.",
9857
+ inputSchema: {
9858
+ type: "object",
9859
+ properties: {
9860
+ points: POINTS,
9861
+ source: SOURCE,
9862
+ aligned: { type: "boolean", description: "Keep the offset for the next strokes (default the current option, true)." },
9863
+ sample: SAMPLE,
9864
+ brush: BRUSH2,
9865
+ ...TARGET2
9866
+ },
9867
+ required: ["points"],
9868
+ additionalProperties: false
9869
+ },
9870
+ annotations: { title: "Clone Stamp stroke", ...WRITE5 },
9871
+ handler: (args) => stroke("clone", args, pick(args, ["aligned", "sample"]))
9872
+ };
9873
+ const healBrush = {
9874
+ name: "heal_brush",
9875
+ title: "Healing Brush stroke",
9876
+ description: "Heals along a stroke from the clone source (Photoshop's Healing Brush): the source's texture is cloned, then blended so its colour and light match the pixels around the stroke (Poisson seamless cloning, applied when the stroke ends). Use it for blemishes and dust where the area around them is the right tone; use clone_stamp to copy exactly. Same source, offset and aligned rules as clone_stamp. One undo step. Returns the layerId, channel, changed bounds, dab count and clone {offset, source, aligned}.",
9877
+ inputSchema: {
9878
+ type: "object",
9879
+ properties: {
9880
+ points: POINTS,
9881
+ source: SOURCE,
9882
+ aligned: { type: "boolean", description: "Keep the offset for the next strokes (default the current option, true)." },
9883
+ sample: SAMPLE,
9884
+ brush: BRUSH2,
9885
+ ...TARGET2
9886
+ },
9887
+ required: ["points"],
9888
+ additionalProperties: false
9889
+ },
9890
+ annotations: { title: "Healing Brush stroke", ...WRITE5 },
9891
+ handler: (args) => stroke("heal", args, pick(args, ["aligned", "sample"]))
9892
+ };
9893
+ const spotHeal = {
9894
+ name: "spot_heal",
9895
+ title: "Spot Healing stroke",
9896
+ description: "Removes what a stroke covers without a source point (Photoshop's Spot Healing Brush): proximity-match copies the best matching patch nearby and blends it in; smooth fills from the edges inwards (for spots on smooth areas). Paint just over the spot with a brush a little larger than it. One undo step. Returns the layerId, channel, changed bounds, dab count and spotHeal {offset} (the patch offset copied, or null for a smooth fill).",
9897
+ inputSchema: {
9898
+ type: "object",
9899
+ properties: {
9900
+ points: POINTS,
9901
+ type: { type: "string", enum: [...SPOT_HEAL_TYPES], description: "proximity-match (default) or smooth." },
9902
+ sample: SAMPLE,
9903
+ brush: BRUSH2,
9904
+ ...TARGET2
9905
+ },
9906
+ required: ["points"],
9907
+ additionalProperties: false
9908
+ },
9909
+ annotations: { title: "Spot Healing stroke", ...WRITE5 },
9910
+ handler: (args) => stroke("spot-heal", args, { ...pick(args, ["sample"]), ...args.type !== void 0 ? { spotType: args.type } : {} })
9911
+ };
9912
+ const smudge = {
9913
+ name: "smudge",
9914
+ title: "Smudge stroke",
9915
+ description: "Drags pixels along a stroke like a finger through wet paint (Photoshop's Smudge): each dab lays what the finger carries and picks up what was under it, so colour is pulled in the stroke's direction and fades with distance. One undo step. Returns the layerId, channel, changed bounds and dab count.",
9916
+ inputSchema: {
9917
+ type: "object",
9918
+ properties: {
9919
+ points: POINTS,
9920
+ strength: { type: "number", minimum: 0, maximum: 1, description: "How far colour is dragged, 0..1 (default the current option, 0.5)." },
9921
+ brush: BRUSH2,
9922
+ ...TARGET2
9923
+ },
9924
+ required: ["points"],
9925
+ additionalProperties: false
9926
+ },
9927
+ annotations: { title: "Smudge stroke", ...WRITE5 },
9928
+ handler: (args) => stroke("smudge", args, pick(args, ["strength"]))
9929
+ };
9930
+ const toneSchema = (verb) => ({
9931
+ type: "object",
9932
+ properties: {
9933
+ points: POINTS,
9934
+ range: { type: "string", enum: [...TONE_RANGES], description: `Which tones to ${verb} most: shadows, midtones (default the current option, midtones) or highlights.` },
9935
+ exposure: { type: "number", minimum: 0, maximum: 1, description: "Strength 0..1 (default the current option, 0.5). Overlapping dabs in one stroke never go past it; another stroke builds on it." },
9936
+ protectTones: { type: "boolean", description: "Change luma only, keeping hue and saturation (default true)." },
9937
+ brush: BRUSH2,
9938
+ ...TARGET2
9939
+ },
9940
+ required: ["points"],
9941
+ additionalProperties: false
9942
+ });
9943
+ const dodge = {
9944
+ name: "dodge",
9945
+ title: "Dodge stroke",
9946
+ description: "Lightens the pixels a stroke covers (Photoshop's Dodge), most in the chosen tonal range. Alpha is kept. One undo step. Returns the layerId, channel, changed bounds and dab count. Inspect the result with capture_view or get_histogram; several light strokes look more natural than one strong one.",
9947
+ inputSchema: toneSchema("lighten"),
9948
+ annotations: { title: "Dodge stroke", ...WRITE5 },
9949
+ handler: (args) => stroke("dodge", args, pick(args, ["range", "exposure", "protectTones"]))
9950
+ };
9951
+ const burn = {
9952
+ name: "burn",
9953
+ title: "Burn stroke",
9954
+ description: "Darkens the pixels a stroke covers (Photoshop's Burn), most in the chosen tonal range. Alpha is kept. One undo step. Returns the layerId, channel, changed bounds and dab count.",
9955
+ inputSchema: toneSchema("darken"),
9956
+ annotations: { title: "Burn stroke", ...WRITE5 },
9957
+ handler: (args) => stroke("burn", args, pick(args, ["range", "exposure", "protectTones"]))
9958
+ };
9959
+ const spongeTool = {
9960
+ name: "sponge",
9961
+ title: "Sponge stroke",
9962
+ description: "Saturates or desaturates the pixels a stroke covers (Photoshop's Sponge); passes compound, and brush.flow sets how much each moves. Pixels only (a mask has no colour). One undo step. Returns the layerId, channel, changed bounds and dab count.",
9963
+ inputSchema: {
9964
+ type: "object",
9965
+ properties: {
9966
+ points: POINTS,
9967
+ mode: { type: "string", enum: ["desaturate", "saturate"], description: "desaturate (default the current option, desaturate) or saturate." },
9968
+ vibrance: { type: "boolean", description: "Damp the change where colours would clip or collapse to grey (default true)." },
9969
+ brush: BRUSH2,
9970
+ layerId: TARGET2.layerId
9971
+ },
9972
+ required: ["points"],
9973
+ additionalProperties: false
9974
+ },
9975
+ annotations: { title: "Sponge stroke", ...WRITE5 },
9976
+ handler: (args) => stroke("sponge", { ...args, channel: "pixels" }, { ...pick(args, ["vibrance"]), ...args.mode !== void 0 ? { spongeMode: args.mode } : {} })
9977
+ };
9978
+ const getOptions = {
9979
+ name: "get_retouch_options",
9980
+ title: "Get retouch options",
9981
+ description: "Reads the retouching tools' options (aligned, sample, spotType, range, exposure, protectTones, spongeMode, vibrance, strength) and the clone source ({x, y, offset} or null; offset is the aligned offset the last clone or heal stroke fixed). Call it before clone_stamp or heal_brush without a source to see where they will sample.",
9982
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
9983
+ annotations: { title: "Get retouch options", ...READ5 },
9984
+ handler: () => {
9985
+ const { retouch, cloneSource } = controller.getState().paint;
9986
+ return { options: { ...retouch }, cloneSource: cloneSource ? { ...cloneSource } : null };
9987
+ }
9988
+ };
9989
+ const setOptions = {
9990
+ name: "set_retouch_options",
9991
+ title: "Set retouch options",
9992
+ description: "Sets the retouching tools' defaults (view state, not an undo step); each stroke tool can also override them per call. Changing aligned clears the aligned offset. Returns the options after the change.",
9993
+ inputSchema: {
9994
+ type: "object",
9995
+ properties: {
9996
+ aligned: { type: "boolean", description: "Clone / heal: keep the offset from stroke to stroke (default true)." },
9997
+ sample: SAMPLE,
9998
+ spotType: { type: "string", enum: [...SPOT_HEAL_TYPES], description: "Spot heal: proximity-match (default) or smooth." },
9999
+ range: { type: "string", enum: [...TONE_RANGES], description: "Dodge / burn: shadows, midtones (default) or highlights." },
10000
+ exposure: { type: "number", minimum: 0, maximum: 1, description: "Dodge / burn strength 0..1 (default 0.5)." },
10001
+ protectTones: { type: "boolean", description: "Dodge / burn: keep hue and saturation (default true)." },
10002
+ spongeMode: { type: "string", enum: ["desaturate", "saturate"], description: "Sponge: desaturate (default) or saturate." },
10003
+ vibrance: { type: "boolean", description: "Sponge: damp clipping (default true)." },
10004
+ strength: { type: "number", minimum: 0, maximum: 1, description: "Smudge strength 0..1 (default 0.5)." }
10005
+ },
10006
+ additionalProperties: false
10007
+ },
10008
+ annotations: { title: "Set retouch options", ...WRITE5, idempotentHint: true },
10009
+ handler: (args) => ({ ...controller.setPaintOptions({ retouch: args }).retouch })
10010
+ };
10011
+ const setSource = {
10012
+ name: "set_clone_source",
10013
+ title: "Set the clone source",
10014
+ description: "Sets the Clone Stamp and Healing Brush source point (Photoshop's Alt-click), in document pixels, or clears it with clear true. The next clone_stamp or heal_brush stroke fixes the offset from its first point. View state, not an undo step. Returns the clone source {x, y, offset} (or null).",
10015
+ inputSchema: {
10016
+ type: "object",
10017
+ properties: {
10018
+ x: { type: "number", description: "Source x in px (required unless clear)." },
10019
+ y: { type: "number", description: "Source y in px (required unless clear)." },
10020
+ clear: { type: "boolean", description: "Remove the source point instead (default false)." }
10021
+ },
10022
+ additionalProperties: false
10023
+ },
10024
+ annotations: { title: "Set the clone source", ...WRITE5, idempotentHint: true },
10025
+ handler: (args) => {
10026
+ if (args.clear === true) {
10027
+ if (args.x !== void 0 || args.y !== void 0) throw new LayerOperationError("Give either clear true or x and y, not both.");
10028
+ return controller.setCloneSource(null);
10029
+ }
10030
+ if (typeof args.x !== "number" || typeof args.y !== "number") throw new LayerOperationError("Pass x and y (document pixels) for the source point, or clear true to remove it.");
10031
+ return controller.setCloneSource(checkSource(controller, { x: args.x, y: args.y }));
10032
+ }
10033
+ };
10034
+ return [cloneStamp, healBrush, spotHeal, smudge, dodge, burn, spongeTool, getOptions, setOptions, setSource];
10035
+ }
10036
+ function pick(args, keys) {
10037
+ const out = {};
10038
+ for (const k of keys) if (args[k] !== void 0) out[k] = args[k];
10039
+ return out;
10040
+ }
10041
+
10042
+ // src/image-editor/psd-tools.ts
10043
+ var READ6 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
9496
10044
  var DESTRUCTIVE4 = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
9497
10045
  var DATA = {
9498
10046
  type: "string",
@@ -9513,7 +10061,7 @@ function createImagePsdTools(controller) {
9513
10061
  title: "Inspect a PSD",
9514
10062
  description: "Reads a PSD / PSB without opening it: size, bit depth, colour mode, resolution, and every layer (bottom first, a group before its children) with its PSD kind, the layer type it would become and its status (mapped, approximated or dropped, with notes), plus the import warnings. Changes nothing. Call before import_psd to see what would be lost.",
9515
10063
  inputSchema: { type: "object", properties: { data: DATA }, required: ["data"], additionalProperties: false },
9516
- annotations: { title: "Inspect a PSD", ...READ5 },
10064
+ annotations: { title: "Inspect a PSD", ...READ6 },
9517
10065
  handler: ({ data }) => {
9518
10066
  const res = importPsd(psdBytesFromBase64(data), { skipPixels: true });
9519
10067
  return {
@@ -9550,7 +10098,7 @@ function createImagePsdTools(controller) {
9550
10098
  title: "Export the document as a PSD",
9551
10099
  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.",
9552
10100
  inputSchema: { type: "object", properties: {}, additionalProperties: false },
9553
- annotations: { title: "Export the document as a PSD", ...READ5 },
10101
+ annotations: { title: "Export the document as a PSD", ...READ6 },
9554
10102
  handler: async () => {
9555
10103
  const res = await controller.toPsd();
9556
10104
  return {
@@ -9568,8 +10116,8 @@ function createImagePsdTools(controller) {
9568
10116
  }
9569
10117
 
9570
10118
  // src/image-editor/transform-tools.ts
9571
- var READ6 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
9572
- var WRITE5 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
10119
+ var READ7 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
10120
+ var WRITE6 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
9573
10121
  var round4 = (n, digits = 3) => Math.round(n * 10 ** digits) / 10 ** digits;
9574
10122
  var roundPoint = (p) => ({ x: round4(p.x), y: round4(p.y) });
9575
10123
  var POINT = {
@@ -9578,7 +10126,7 @@ var POINT = {
9578
10126
  required: ["x", "y"],
9579
10127
  additionalProperties: false
9580
10128
  };
9581
- var TARGET2 = {
10129
+ var TARGET3 = {
9582
10130
  layerId: { type: "string", description: "Layer to transform (from list_layers; default the active layer)." },
9583
10131
  content: {
9584
10132
  type: "string",
@@ -9660,15 +10208,15 @@ function createImageTransformTools(controller) {
9660
10208
  inputSchema: {
9661
10209
  type: "object",
9662
10210
  properties: {
9663
- layerId: TARGET2.layerId,
9664
- content: { type: "string", enum: FREE_TRANSFORM_CONTENTS, description: `${TARGET2.content.description} selection-outline frames the selection outline alone.` },
9665
- channel: TARGET2.channel,
10211
+ layerId: TARGET3.layerId,
10212
+ content: { type: "string", enum: FREE_TRANSFORM_CONTENTS, description: `${TARGET3.content.description} selection-outline frames the selection outline alone.` },
10213
+ channel: TARGET3.channel,
9666
10214
  meshColumns: { type: "integer", minimum: 1, maximum: 16, description: "Patch columns of an identity warp mesh to return (omit for none)." },
9667
10215
  meshRows: { type: "integer", minimum: 1, maximum: 16, description: "Patch rows of an identity warp mesh to return (default meshColumns)." }
9668
10216
  },
9669
10217
  additionalProperties: false
9670
10218
  },
9671
- annotations: { title: "Get the Free Transform frame", ...READ6 },
10219
+ annotations: { title: "Get the Free Transform frame", ...READ7 },
9672
10220
  handler: (args) => {
9673
10221
  const init = targetFrom(args);
9674
10222
  if (init.content === "selection-copy") init.content = "selection";
@@ -9698,10 +10246,10 @@ function createImageTransformTools(controller) {
9698
10246
  description: "Edit \u203A Free Transform: scales, rotates, skews, moves, distorts or puts in perspective a pixel layer (with its linked mask), the selected pixels, a copy of them on a new layer, or an unlinked mask. Give the transform one way: the numeric fields (as Photoshop\u2019s options bar, about a reference point), a matrix, or the frame\u2019s new corners (distort / perspective; read the frame with get_transform_frame). Resamples once, tile by tile; one undo step. Returns layerId (the new layer for selection-copy), what moved, the frame, the corners and the new document bounds.",
9699
10247
  inputSchema: {
9700
10248
  type: "object",
9701
- properties: { ...TARGET2, ...GEOMETRY },
10249
+ properties: { ...TARGET3, ...GEOMETRY },
9702
10250
  additionalProperties: false
9703
10251
  },
9704
- annotations: { title: "Free Transform a layer", ...WRITE5 },
10252
+ annotations: { title: "Free Transform a layer", ...WRITE6 },
9705
10253
  handler: (args) => summary2(controller.transformLayer({ ...targetFrom(args), ...geometryFrom(args) }))
9706
10254
  };
9707
10255
  const warpLayer = {
@@ -9711,7 +10259,7 @@ function createImageTransformTools(controller) {
9711
10259
  inputSchema: {
9712
10260
  type: "object",
9713
10261
  properties: {
9714
- ...TARGET2,
10262
+ ...TARGET3,
9715
10263
  style: { type: "string", enum: WARP_STYLES, description: "Preset warp (Photoshop\u2019s menu): arc, arcLower, arcUpper, arch, bulge, shellLower, shellUpper, flag, wave, fish, rise, fisheye, inflate, squeeze, twist." },
9716
10264
  bend: { type: "number", minimum: -100, maximum: 100, description: "Preset bend in percent, \u2212100..100 (default 50)." },
9717
10265
  horizontal: { type: "number", minimum: -100, maximum: 100, description: "Preset horizontal distortion in percent, \u2212100..100 (default 0)." },
@@ -9731,7 +10279,7 @@ function createImageTransformTools(controller) {
9731
10279
  },
9732
10280
  additionalProperties: false
9733
10281
  },
9734
- annotations: { title: "Warp a layer", ...WRITE5 },
10282
+ annotations: { title: "Warp a layer", ...WRITE6 },
9735
10283
  handler: (args) => {
9736
10284
  const hasPreset = ["style", "bend", "horizontal", "vertical", "orientation"].some((k) => args[k] !== void 0);
9737
10285
  if (hasPreset && args.mesh !== void 0) throw new LayerOperationError("Give a preset (style, bend, horizontal, vertical, orientation) or a mesh, not both.");
@@ -9762,10 +10310,10 @@ function createImageTransformTools(controller) {
9762
10310
  description: "Select \u203A Transform Selection: scales, rotates, skews, moves or distorts only the selection outline; no pixels change. Give the transform one way: the numeric fields (about a reference point of the selection\u2019s bounds), a matrix, or the new corners of its bounds. Needs a selection. One undo step. Returns the new selection bounds.",
9763
10311
  inputSchema: {
9764
10312
  type: "object",
9765
- properties: { interpolation: { ...TARGET2.interpolation, description: "Resampling of soft edges: nearest, bilinear or bicubic (default)." }, ...GEOMETRY },
10313
+ properties: { interpolation: { ...TARGET3.interpolation, description: "Resampling of soft edges: nearest, bilinear or bicubic (default)." }, ...GEOMETRY },
9766
10314
  additionalProperties: false
9767
10315
  },
9768
- annotations: { title: "Transform the selection outline", ...WRITE5 },
10316
+ annotations: { title: "Transform the selection outline", ...WRITE6 },
9769
10317
  handler: (args) => {
9770
10318
  const res = controller.transformLayer({ content: "selection-outline", interpolation: args.interpolation, ...geometryFrom(args) });
9771
10319
  return { changed: res.changed, frame: { ...res.box }, corners: res.corners.map(roundPoint), bounds: res.bounds };
@@ -9779,14 +10327,14 @@ function createImageTransformTools(controller) {
9779
10327
  type: "object",
9780
10328
  properties: {
9781
10329
  axis: { type: "string", enum: ["horizontal", "vertical"], description: "horizontal mirrors left \u2194 right, vertical top \u2194 bottom." },
9782
- layerId: TARGET2.layerId,
9783
- content: TARGET2.content,
9784
- channel: TARGET2.channel
10330
+ layerId: TARGET3.layerId,
10331
+ content: TARGET3.content,
10332
+ channel: TARGET3.channel
9785
10333
  },
9786
10334
  required: ["axis"],
9787
10335
  additionalProperties: false
9788
10336
  },
9789
- annotations: { title: "Flip a layer", ...WRITE5 },
10337
+ annotations: { title: "Flip a layer", ...WRITE6 },
9790
10338
  handler: (args) => summary2(controller.flipLayer(args.axis, targetFrom(args)))
9791
10339
  };
9792
10340
  const rotate90 = {
@@ -9797,22 +10345,22 @@ function createImageTransformTools(controller) {
9797
10345
  type: "object",
9798
10346
  properties: {
9799
10347
  turn: { type: "integer", enum: [90, -90, 180], description: "Degrees: 90 clockwise, -90 counter-clockwise, or 180." },
9800
- layerId: TARGET2.layerId,
9801
- content: TARGET2.content,
9802
- channel: TARGET2.channel
10348
+ layerId: TARGET3.layerId,
10349
+ content: TARGET3.content,
10350
+ channel: TARGET3.channel
9803
10351
  },
9804
10352
  required: ["turn"],
9805
10353
  additionalProperties: false
9806
10354
  },
9807
- annotations: { title: "Rotate a layer by quarter turns", ...WRITE5 },
10355
+ annotations: { title: "Rotate a layer by quarter turns", ...WRITE6 },
9808
10356
  handler: (args) => summary2(controller.rotateLayer90(args.turn, targetFrom(args)))
9809
10357
  };
9810
10358
  return [getFrame, transformLayer, warpLayer, transformSelection, flipLayer, rotate90];
9811
10359
  }
9812
10360
 
9813
10361
  // src/image-editor/smart-tools.ts
9814
- var READ7 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
9815
- var WRITE6 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
10362
+ var READ8 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
10363
+ var WRITE7 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
9816
10364
  var WRITE_IDEMPOTENT4 = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
9817
10365
  var DESTRUCTIVE5 = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
9818
10366
  var round5 = (n, digits = 3) => Math.round(n * 10 ** digits) / 10 ** digits;
@@ -9870,7 +10418,7 @@ function createImageSmartObjectTools(controller) {
9870
10418
  title: "Read a smart object",
9871
10419
  description: "Returns a smart object\u2019s contents (id shared by linked instances, name, kind raster or document, pixel size, the file type it came from, the instances placed from it), its placement (corners in document pixels, warp mesh in contents pixels, interpolation, and scale / rotate / skew in percent and degrees of the contents\u2019 own size when affine), its smart filters bottom to top (ids, kind, settings, enabled, blend, opacity; kind psd marks a Photoshop filter kept but not drawn) with the filter mask, and the rendered bounds. Changes nothing.",
9872
10420
  inputSchema: { type: "object", properties: { layerId: LAYER_ID4 }, required: ["layerId"], additionalProperties: false },
9873
- annotations: { title: "Read a smart object", ...READ7 },
10421
+ annotations: { title: "Read a smart object", ...READ8 },
9874
10422
  handler: ({ layerId }) => info(smartId(layerId))
9875
10423
  };
9876
10424
  const convert = {
@@ -9885,7 +10433,7 @@ function createImageSmartObjectTools(controller) {
9885
10433
  },
9886
10434
  additionalProperties: false
9887
10435
  },
9888
- annotations: { title: "Convert to Smart Object", ...WRITE6 },
10436
+ annotations: { title: "Convert to Smart Object", ...WRITE7 },
9889
10437
  handler: ({ layerIds, name }) => {
9890
10438
  for (const id2 of layerIds ?? []) requireLayer(controller.getLayers(), id2);
9891
10439
  const id = controller.convertToSmartObject(layerIds, { name });
@@ -10004,7 +10552,7 @@ function createImageSmartObjectTools(controller) {
10004
10552
  title: "Export a smart object\u2019s contents",
10005
10553
  description: "Layer \u203A Smart Objects \u203A Export Contents: the contents as a file, base64: the file they came from while unedited (a PSB, PNG, JPEG\u2026), else a PNG for raster contents or a PSD / PSB of a nested document. Returns name, mimeType, byteLength and base64. Changes nothing.",
10006
10554
  inputSchema: { type: "object", properties: { layerId: LAYER_ID4 }, required: ["layerId"], additionalProperties: false },
10007
- annotations: { title: "Export a smart object\u2019s contents", ...READ7 },
10555
+ annotations: { title: "Export a smart object\u2019s contents", ...READ8 },
10008
10556
  handler: ({ layerId }) => {
10009
10557
  const f = controller.exportSmartObjectContents(smartId(layerId));
10010
10558
  return { name: f.name, mimeType: f.mimeType, byteLength: f.bytes.length, base64: bytesToBase64(f.bytes) };
@@ -10024,7 +10572,7 @@ function createImageSmartObjectTools(controller) {
10024
10572
  required: ["layerId"],
10025
10573
  additionalProperties: false
10026
10574
  },
10027
- annotations: { title: "Copy a smart object", ...WRITE6 },
10575
+ annotations: { title: "Copy a smart object", ...WRITE7 },
10028
10576
  handler: ({ layerId, linked, name }) => {
10029
10577
  const id = controller.copySmartObject(smartId(layerId), linked === true, name);
10030
10578
  return { layer: layerSummary(id), contentsId: controller.getSmartObject(id).contents.id };
@@ -10035,7 +10583,7 @@ function createImageSmartObjectTools(controller) {
10035
10583
  title: "Edit a smart object\u2019s contents",
10036
10584
  description: 'Layer \u203A Smart Objects \u203A Edit Contents: opens the contents as a document of their own (a layer document, or raster contents as one pixel layer "Layer 0") with their own undo history. Every other tool (list_layers, paint, adjustments\u2026) then works inside the contents until close_smart_object_contents saves them back. Contents can nest. Returns the contents\u2019 size and layers.',
10037
10585
  inputSchema: { type: "object", properties: { layerId: LAYER_ID4 }, required: ["layerId"], additionalProperties: false },
10038
- annotations: { title: "Edit a smart object\u2019s contents", ...WRITE6 },
10586
+ annotations: { title: "Edit a smart object\u2019s contents", ...WRITE7 },
10039
10587
  handler: ({ layerId }) => {
10040
10588
  const res = controller.editSmartObjectContents(smartId(layerId));
10041
10589
  const s = controller.getState();
@@ -10051,7 +10599,7 @@ function createImageSmartObjectTools(controller) {
10051
10599
  properties: { save: { type: "boolean", description: "true (default) saves the edits into the smart object; false discards them." } },
10052
10600
  additionalProperties: false
10053
10601
  },
10054
- annotations: { title: "Close a smart object\u2019s contents", ...WRITE6 },
10602
+ annotations: { title: "Close a smart object\u2019s contents", ...WRITE7 },
10055
10603
  handler: ({ save }) => ({ ...controller.closeSmartObjectContents(save !== false), editing: controller.getEditingContents() })
10056
10604
  };
10057
10605
  const addFilter = {
@@ -10069,7 +10617,7 @@ function createImageSmartObjectTools(controller) {
10069
10617
  required: ["layerId", "kind"],
10070
10618
  additionalProperties: false
10071
10619
  },
10072
- annotations: { title: "Add a smart filter", ...WRITE6 },
10620
+ annotations: { title: "Add a smart filter", ...WRITE7 },
10073
10621
  handler: (args) => {
10074
10622
  const id = smartId(args.layerId);
10075
10623
  const filter = controller.addSmartFilter(id, { kind: args.kind, ...settingsFrom(args) }, args.index);
@@ -10158,7 +10706,7 @@ function createImageSmartObjectTools(controller) {
10158
10706
  required: ["layerId", "from"],
10159
10707
  additionalProperties: false
10160
10708
  },
10161
- annotations: { title: "Set the smart filter mask", ...WRITE6 },
10709
+ annotations: { title: "Set the smart filter mask", ...WRITE7 },
10162
10710
  handler: (args) => {
10163
10711
  const id = smartId(args.layerId);
10164
10712
  const props = {};
@@ -10171,8 +10719,8 @@ function createImageSmartObjectTools(controller) {
10171
10719
  }
10172
10720
 
10173
10721
  // src/image-editor/shape-tools.ts
10174
- var READ8 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
10175
- var WRITE7 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
10722
+ var READ9 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
10723
+ var WRITE8 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
10176
10724
  var WRITE_IDEMPOTENT5 = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
10177
10725
  var DESTRUCTIVE6 = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
10178
10726
  var LAYER_ID5 = { type: "string", description: "Shape layer id from list_layers (type shape) or add_shape_layer." };
@@ -10425,7 +10973,7 @@ function createImageShapeTools(controller) {
10425
10973
  title: "Read a shape layer",
10426
10974
  description: "Returns a shape layer\u2019s components bottom to top (index, path operation, the live shape it was drawn as or null, its subpaths with absolute anchors and handles, bounds), the fill rule, fill and stroke paints and settings, the path bounds and the rendered bounds (stroke included). Changes nothing.",
10427
10975
  inputSchema: { type: "object", properties: { layerId: LAYER_ID5 }, required: ["layerId"], additionalProperties: false },
10428
- annotations: { title: "Read a shape layer", ...READ8 },
10976
+ annotations: { title: "Read a shape layer", ...READ9 },
10429
10977
  handler: ({ layerId }) => controller.getShapeLayer(shapeId(layerId))
10430
10978
  };
10431
10979
  const add2 = {
@@ -10456,7 +11004,7 @@ function createImageShapeTools(controller) {
10456
11004
  },
10457
11005
  additionalProperties: false
10458
11006
  },
10459
- annotations: { title: "Add a shape layer", ...WRITE7 },
11007
+ annotations: { title: "Add a shape layer", ...WRITE8 },
10460
11008
  handler: (args) => {
10461
11009
  const init = { ...geometry(args) };
10462
11010
  if (args.components !== void 0) init.components = args.components.map(geometry);
@@ -10551,7 +11099,7 @@ function createImageShapeTools(controller) {
10551
11099
  required: ["layerId"],
10552
11100
  additionalProperties: false
10553
11101
  },
10554
- annotations: { title: "Add a shape component", ...WRITE7 },
11102
+ annotations: { title: "Add a shape component", ...WRITE8 },
10555
11103
  handler: ({ layerId, index, ...rest }) => controller.addShapeComponent(shapeId(layerId), geometry(rest), index)
10556
11104
  };
10557
11105
  const removeComponent = {
@@ -10574,8 +11122,8 @@ function createImageShapeTools(controller) {
10574
11122
  }
10575
11123
 
10576
11124
  // 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 };
11125
+ var READ10 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
11126
+ var WRITE9 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
10579
11127
  var WRITE_IDEMPOTENT6 = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
10580
11128
  var DESTRUCTIVE7 = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
10581
11129
  var LAYER_ID6 = { type: "string", description: "Type layer id from list_layers (type type) or add_text_layer." };
@@ -10603,7 +11151,7 @@ var PARAGRAPH = {
10603
11151
  spaceBefore: { type: ["number", "null"], description: "Space above the paragraph in pixels (not before the first)." },
10604
11152
  spaceAfter: { type: ["number", "null"], description: "Space below the paragraph in pixels." }
10605
11153
  };
10606
- function pick(args, keys) {
11154
+ function pick2(args, keys) {
10607
11155
  const out = {};
10608
11156
  for (const k of keys) if (args[k] !== void 0) out[k] = args[k];
10609
11157
  return out;
@@ -10625,7 +11173,7 @@ function createImageTypeTools(controller) {
10625
11173
  title: "List fonts",
10626
11174
  description: "Returns the font families type layers can use here (generic families such as sans-serif always work). Changes nothing.",
10627
11175
  inputSchema: { type: "object", properties: {}, additionalProperties: false },
10628
- annotations: { title: "List fonts", ...READ9 },
11176
+ annotations: { title: "List fonts", ...READ10 },
10629
11177
  handler: () => ({ families: controller.listFonts(), shaper: controller.getTextShaper()?.id ?? null })
10630
11178
  };
10631
11179
  const get = {
@@ -10633,7 +11181,7 @@ function createImageTypeTools(controller) {
10633
11181
  title: "Read a type layer",
10634
11182
  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
11183
  inputSchema: { type: "object", properties: { layerId: LAYER_ID6 }, required: ["layerId"], additionalProperties: false },
10636
- annotations: { title: "Read a type layer", ...READ9 },
11184
+ annotations: { title: "Read a type layer", ...READ10 },
10637
11185
  handler: ({ layerId }) => controller.getTextLayer(typeId(layerId))
10638
11186
  };
10639
11187
  const add2 = {
@@ -10667,9 +11215,9 @@ function createImageTypeTools(controller) {
10667
11215
  required: ["text", "x", "y"],
10668
11216
  additionalProperties: false
10669
11217
  },
10670
- annotations: { title: "Add a type layer", ...WRITE8 },
11218
+ annotations: { title: "Add a type layer", ...WRITE9 },
10671
11219
  handler: (args) => {
10672
- const init = pick(args, ["text", "x", "y", "box", "style", "paragraph", "rotation", "name", "opacity", "blend", "aboveId", "belowId", "parentId", "index"]);
11220
+ const init = pick2(args, ["text", "x", "y", "box", "style", "paragraph", "rotation", "name", "opacity", "blend", "aboveId", "belowId", "parentId", "index"]);
10673
11221
  if (init.blend !== void 0) init.blend = init.blend;
10674
11222
  const id = controller.addTextLayer(init);
10675
11223
  return { layer: layerSummary(id), text: controller.getTextLayer(id) };
@@ -10680,7 +11228,7 @@ function createImageTypeTools(controller) {
10680
11228
  title: "Edit text",
10681
11229
  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
11230
  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 },
11231
+ annotations: { title: "Edit text", ...WRITE9 },
10684
11232
  handler: (args) => controller.setText(typeId(args.layerId), args.text, range(args))
10685
11233
  };
10686
11234
  const setStyle = {
@@ -10690,7 +11238,7 @@ function createImageTypeTools(controller) {
10690
11238
  inputSchema: { type: "object", properties: { layerId: LAYER_ID6, start: START, end: END, ...STYLE }, required: ["layerId"], additionalProperties: false },
10691
11239
  annotations: { title: "Style a range of text", ...WRITE_IDEMPOTENT6 },
10692
11240
  handler: (args) => {
10693
- const style = pick(args, Object.keys(STYLE));
11241
+ const style = pick2(args, Object.keys(STYLE));
10694
11242
  if (!Object.keys(style).length) throw new LayerOperationError(`Give at least one style field: ${Object.keys(STYLE).join(", ")}.`);
10695
11243
  return controller.setTextStyle(typeId(args.layerId), style, range(args));
10696
11244
  }
@@ -10702,7 +11250,7 @@ function createImageTypeTools(controller) {
10702
11250
  inputSchema: { type: "object", properties: { layerId: LAYER_ID6, start: START, end: END, ...PARAGRAPH }, required: ["layerId"], additionalProperties: false },
10703
11251
  annotations: { title: "Style paragraphs", ...WRITE_IDEMPOTENT6 },
10704
11252
  handler: (args) => {
10705
- const style = pick(args, Object.keys(PARAGRAPH));
11253
+ const style = pick2(args, Object.keys(PARAGRAPH));
10706
11254
  if (!Object.keys(style).length) throw new LayerOperationError(`Give at least one paragraph field: ${Object.keys(PARAGRAPH).join(", ")}.`);
10707
11255
  return controller.setParagraphStyle(typeId(args.layerId), style, range(args));
10708
11256
  }
@@ -10986,6 +11534,7 @@ function createImageEditorToolProvider(controller, options = {}) {
10986
11534
  ...createImageTypeTools(controller),
10987
11535
  ...createImageEffectTools(controller),
10988
11536
  ...createImagePaintTools(controller),
11537
+ ...createImageRetouchTools(controller),
10989
11538
  ...createPigmentTools(),
10990
11539
  ...createImagePsdTools(controller)
10991
11540
  ]
@@ -12781,6 +13330,7 @@ export {
12781
13330
  DEFAULT_MITER_LIMIT,
12782
13331
  DEFAULT_MIXER,
12783
13332
  DEFAULT_REFINE_EDGE_PARAMS,
13333
+ DEFAULT_RETOUCH,
12784
13334
  DEFAULT_SELECTION_TOOL_OPTIONS,
12785
13335
  DEFAULT_SHAPE_TOOL_OPTIONS,
12786
13336
  DEFAULT_SMUDGE,
@@ -12850,6 +13400,13 @@ export {
12850
13400
  PatternTile,
12851
13401
  PsdError,
12852
13402
  REFINE_EDGE_CONTROLS,
13403
+ RETOUCH_CHOICES,
13404
+ RETOUCH_OPTION_CONTROLS,
13405
+ RETOUCH_SAMPLES,
13406
+ RETOUCH_TOGGLES,
13407
+ RETOUCH_TOOLS,
13408
+ RegionPixelSource,
13409
+ RetouchDabStroke,
12853
13410
  SELECTION_MODES,
12854
13411
  SELECTION_MODE_LABELS,
12855
13412
  SELECTION_MODIFY_OPS,
@@ -12859,6 +13416,7 @@ export {
12859
13416
  SHAPE_OPERATIONS,
12860
13417
  SHAPE_RENDER_MARGIN,
12861
13418
  SMART_RENDER_MARGIN,
13419
+ SPOT_HEAL_TYPES,
12862
13420
  SUPPORTED_ADJUSTMENT_KEYS,
12863
13421
  SelectionGesture,
12864
13422
  ShapeDrawGesture,
@@ -12872,10 +13430,12 @@ export {
12872
13430
  TILE_SHIFT,
12873
13431
  TINT_SCALE,
12874
13432
  TONE_LUT_SIZE,
13433
+ TONE_RANGES,
12875
13434
  TONE_RANGE_PRESETS,
12876
13435
  TRANSFORM_ANCHORS,
12877
13436
  TextEditSession,
12878
13437
  TileMedium,
13438
+ TilePixelSource,
12879
13439
  TiledSurface,
12880
13440
  TransformTooLargeError,
12881
13441
  TypeToolGesture,
@@ -12917,12 +13477,14 @@ export {
12917
13477
  applyProcess2Adjustments,
12918
13478
  applyRegionBlur,
12919
13479
  applyRegionMosaic,
13480
+ applyRetouchToTile,
12920
13481
  applyShapeOptions,
12921
13482
  applySmartFilters,
12922
13483
  applyStrokeToTile,
12923
13484
  backgroundLayer,
12924
13485
  base64ToBytes,
12925
13486
  baseProjection,
13487
+ bestOffset,
12926
13488
  blackWhiteGray,
12927
13489
  blendChannel,
12928
13490
  blendImageData,
@@ -12952,6 +13514,7 @@ export {
12952
13514
  clampControlValue,
12953
13515
  clampCropToImage,
12954
13516
  cleanMixer,
13517
+ cloneSourceLabel,
12955
13518
  collectTiles,
12956
13519
  colorBalanceChannel,
12957
13520
  colorRange,
@@ -12983,6 +13546,7 @@ export {
12983
13546
  createImageLayerTools,
12984
13547
  createImagePaintTools,
12985
13548
  createImagePsdTools,
13549
+ createImageRetouchTools,
12986
13550
  createImageSelectionTools,
12987
13551
  createImageShapeTools,
12988
13552
  createImageSmartObjectTools,
@@ -13029,8 +13593,11 @@ export {
13029
13593
  developFilter,
13030
13594
  developFromAdjustments,
13031
13595
  developTarget,
13596
+ dilateMask,
13032
13597
  distanceTransform,
13033
13598
  documentContents,
13599
+ dodgeBurn,
13600
+ dodgeBurnCurve,
13034
13601
  downloadPsd,
13035
13602
  dragWarpMeshPoint,
13036
13603
  dropPlacement,
@@ -13092,6 +13659,7 @@ export {
13092
13659
  hasWarp,
13093
13660
  hashMaskContent,
13094
13661
  hashNoise,
13662
+ healPixels,
13095
13663
  hexToRgb,
13096
13664
  histogramPath,
13097
13665
  hitTestAnnotation,
@@ -13135,6 +13703,8 @@ export {
13135
13703
  isQuarterTurn,
13136
13704
  isQuarterTurnSwapped,
13137
13705
  isRenderedLayer,
13706
+ isRetouchPaintTool,
13707
+ isRetouchTool,
13138
13708
  isSelectionEmpty,
13139
13709
  isSelectionMode,
13140
13710
  isSeparableBlendMode,
@@ -13176,13 +13746,16 @@ export {
13176
13746
  liveShapeSubpaths,
13177
13747
  loadImageEditorDocument,
13178
13748
  loadMixer,
13749
+ luma,
13179
13750
  magicWand,
13180
13751
  mapCropHandle,
13181
13752
  mapLayer,
13182
13753
  mapSubpaths,
13754
+ maskBounds,
13183
13755
  maskSummary,
13184
13756
  matrixFromCorners,
13185
13757
  matrixFromParams,
13758
+ membraneFill,
13186
13759
  mergeLayerEffect,
13187
13760
  mergeShapeComponents,
13188
13761
  migrateSerializedImageEditorState,
@@ -13217,6 +13790,7 @@ export {
13217
13790
  normalizeProcessVersion,
13218
13791
  normalizeRect,
13219
13792
  normalizeRefineEdgeParams,
13793
+ normalizeRetouch,
13220
13794
  normalizeShapePaint,
13221
13795
  normalizeShapeStroke,
13222
13796
  normalizeSmartFilter,
@@ -13243,6 +13817,7 @@ export {
13243
13817
  parseVersionedDescriptor,
13244
13818
  patchParagraphStyle,
13245
13819
  patchTextStyle,
13820
+ perDabStrength,
13246
13821
  photoFilterMatrix,
13247
13822
  pigment8,
13248
13823
  pixelLayerBounds,
@@ -13279,6 +13854,7 @@ export {
13279
13854
  readPixel,
13280
13855
  readPsd,
13281
13856
  readRegion,
13857
+ readSourceRegion,
13282
13858
  rectSelection,
13283
13859
  rectToQuad,
13284
13860
  reduceSurface,
@@ -13320,11 +13896,14 @@ export {
13320
13896
  rgbToHsl,
13321
13897
  rgbToReflectance,
13322
13898
  rgbUnitToHex,
13899
+ rgbaViewOfGrey,
13323
13900
  runLayerFilters,
13324
13901
  runLayerMenuAction,
13325
13902
  sampleLut,
13326
13903
  sampleToneLut,
13904
+ saturationOf,
13327
13905
  scaleLayerEffect,
13906
+ seamlessClone,
13328
13907
  selectionArea,
13329
13908
  selectionFromCoverage,
13330
13909
  selectionFromPixels,
@@ -13347,6 +13926,7 @@ export {
13347
13926
  setLayerProperties,
13348
13927
  setLum,
13349
13928
  setPigmentColor,
13929
+ setRetouchChoice,
13350
13930
  setSmartFilterMask,
13351
13931
  setSmartFiltersEnabled,
13352
13932
  setSmartPlacement,
@@ -13361,9 +13941,15 @@ export {
13361
13941
  smartObjectLayers,
13362
13942
  smoothCoverage,
13363
13943
  smoothPoints,
13944
+ smudgeEffect,
13364
13945
  solidSurface,
13946
+ solveMembrane,
13365
13947
  spliceRuns,
13366
13948
  splitOverlayAnnotations,
13949
+ sponge,
13950
+ spongeEffect,
13951
+ spotHealPixels,
13952
+ spotHealWindow,
13367
13953
  srgb8ToLab,
13368
13954
  srgbToLinear,
13369
13955
  standardBlendMode,
@@ -13389,6 +13975,7 @@ export {
13389
13975
  toTextSpace,
13390
13976
  toneCurve,
13391
13977
  toneLuts,
13978
+ toneOf,
13392
13979
  toneRange,
13393
13980
  transformHomography,
13394
13981
  transformMap,