@pixerate/editor 0.202.0 → 0.203.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.
@@ -10071,6 +10071,46 @@ var ImageEditorController = class {
10071
10071
  this.setSelection(sel, "replace");
10072
10072
  return this.state.selection ?? null;
10073
10073
  }
10074
+ /**
10075
+ * Asks the segmentation provider (which must be set) for `request.target` in the flattened image
10076
+ * and returns its results with masks decoded to pixels.
10077
+ */
10078
+ async modelSegments(request) {
10079
+ const provider = this.segmentation;
10080
+ const { width, height } = this.naturalSize;
10081
+ const image = await this.toDataURL({ format: "image/png" });
10082
+ const results = await provider.segment({ ...request, image, width, height });
10083
+ request.signal?.throwIfAborted();
10084
+ const decoded = [];
10085
+ for (const r of results ?? []) decoded.push({ ...r, mask: r.mask !== void 0 ? await this.decodeMaskImage(r.mask) : void 0 });
10086
+ return decoded;
10087
+ }
10088
+ /**
10089
+ * The model's object for Object Selection: of what it returns, the pieces mostly inside the box
10090
+ * (clipped to the box grown by a tenth) or those under the point. `null` when none fits.
10091
+ */
10092
+ async modelObject(target, signal) {
10093
+ const { width, height } = this.naturalSize;
10094
+ const decoded = await this.modelSegments(
10095
+ "rect" in target ? { target: "the main object inside the box", box: target.rect, maxSegments: 4, signal } : { target: "the object at the point", point: target.point, maxSegments: 4, signal }
10096
+ );
10097
+ let union = null;
10098
+ for (const r of decoded) {
10099
+ const one = segmentsToSelection(width, height, [r]);
10100
+ if (!one?.bounds) continue;
10101
+ if ("rect" in target) {
10102
+ const r0 = target.rect;
10103
+ const grow = 0.1 * Math.max(r0.width, r0.height);
10104
+ const box = rectSelection(width, height, { x: r0.x - grow, y: r0.y - grow, width: r0.width + 2 * grow, height: r0.height + 2 * grow }, { antiAlias: false });
10105
+ const inside = combineSelections(one, box, "intersect");
10106
+ if (!inside || selectionArea(inside) < selectionArea(one) * 0.5) continue;
10107
+ union = combineSelections(union, inside, "add");
10108
+ } else if (selectionValueAt(one, target.point.x, target.point.y) >= 0.5) {
10109
+ union = combineSelections(union, one, "add");
10110
+ }
10111
+ }
10112
+ return union;
10113
+ }
10074
10114
  /**
10075
10115
  * Object Selection: the object inside `target.rect` (natural pixels; what is outside the box is
10076
10116
  * background) or under `target.point`, found by GrabCut (colour models of object and background
@@ -10088,10 +10128,17 @@ var ImageEditorController = class {
10088
10128
  } else if (!(target.point.x >= 0 && target.point.y >= 0 && target.point.x < width && target.point.y < height)) {
10089
10129
  throw new Error(`(${target.point.x}, ${target.point.y}) is outside the image (0..${width - 1}, 0..${height - 1}); click on the object.`);
10090
10130
  }
10131
+ if (this.segmentation && options.useModel !== false) {
10132
+ const found = await this.modelObject(target, options.signal);
10133
+ if (found) {
10134
+ this.setSelection(found, options.mode ?? "replace");
10135
+ return { selection: this.state.selection ?? null, method: "model" };
10136
+ }
10137
+ }
10091
10138
  const pixels = await this.requirePixels({ sample: options.sampleAllLayers === false ? "active-layer" : "composite" });
10092
10139
  const region = selectObjectIn(pixels, target, { enhanceEdge: options.enhanceEdge });
10093
10140
  this.setSelection(region, options.mode ?? "replace");
10094
- return this.state.selection ?? null;
10141
+ return { selection: this.state.selection ?? null, method: "grabcut" };
10095
10142
  }
10096
10143
  /**
10097
10144
  * Select › Focus Area: selects the parts of the image that are in focus (fine detail above the
@@ -10111,11 +10158,18 @@ var ImageEditorController = class {
10111
10158
  * (the selection is then unchanged), why.
10112
10159
  */
10113
10160
  async selectSky(options = {}) {
10161
+ if (this.segmentation && options.useModel !== false) {
10162
+ const { width, height } = this.naturalSize;
10163
+ const sky = segmentsToSelection(width, height, await this.modelSegments({ target: "the sky", signal: options.signal }));
10164
+ if (!sky?.bounds) return { selection: this.state.selection ?? null, method: "model", note: "The segmentation model found no sky." };
10165
+ this.setSelection(sky, options.mode ?? "replace");
10166
+ return { selection: this.state.selection ?? null, method: "model" };
10167
+ }
10114
10168
  const pixels = await this.requirePixels({ sample: options.sampleAllLayers === false ? "active-layer" : "composite" });
10115
10169
  const { selection, reason } = findSky(pixels, options);
10116
- if (!selection.bounds) return { selection: this.state.selection ?? null, note: reason };
10170
+ if (!selection.bounds) return { selection: this.state.selection ?? null, method: "classical", note: reason };
10117
10171
  this.setSelection(selection, options.mode ?? "replace");
10118
- return { selection: this.state.selection ?? null };
10172
+ return { selection: this.state.selection ?? null, method: "classical" };
10119
10173
  }
10120
10174
  /**
10121
10175
  * Supplies the pixels the selection tools sample (natural size RGBA), e.g. decoded in a worker
@@ -10306,13 +10360,7 @@ var ImageEditorController = class {
10306
10360
  const description = target.trim();
10307
10361
  if (!description) throw new Error("Describe the subject to select.");
10308
10362
  const { width, height } = this.naturalSize;
10309
- const image = await this.toDataURL({ format: "image/png" });
10310
- const results = await provider.segment({ image, width, height, target: description, maxSegments: options.maxSegments, signal: options.signal });
10311
- options.signal?.throwIfAborted();
10312
- const decoded = [];
10313
- for (const r of results ?? []) {
10314
- decoded.push({ ...r, mask: r.mask !== void 0 ? await this.decodeMaskImage(r.mask) : void 0 });
10315
- }
10363
+ const decoded = await this.modelSegments({ target: description, maxSegments: options.maxSegments, signal: options.signal });
10316
10364
  const segments = decoded.map((r) => {
10317
10365
  const one = segmentsToSelection(width, height, [r]);
10318
10366
  return { label: r.label || description, bounds: one?.bounds ?? null, ...r.score !== void 0 ? { score: r.score } : {} };
@@ -13624,7 +13672,7 @@ function createImageSelectionTools(controller) {
13624
13672
  const selectObjectTool = {
13625
13673
  name: "select_object",
13626
13674
  title: "Object Selection",
13627
- description: "Object Selection: selects the object inside rect (a box drawn loosely around it, natural image pixels; what lies outside the box is treated as background) or the object under point, found from the colours of object and background and the edges between them (GrabCut). Combined with the selection by mode. A box is more reliable than a point: make it a little larger than the object on every side. Check with get_selection and capture_view, then fix with quick_select strokes or select_object with mode add / subtract. One undoable step. Returns the selection.",
13675
+ description: 'Object Selection: selects the object inside rect (a box drawn loosely around it, natural image pixels; what lies outside the box is treated as background) or the object under point. With a host segmentation model it asks the model (falling back if nothing it returns fits); otherwise it finds the object from the colours of object and background and the edges between them (GrabCut). Combined with the selection by mode. A box is more reliable than a point: make it a little larger than the object on every side. Check with get_selection and capture_view, then fix with quick_select strokes or select_object with mode add / subtract. One undoable step. Returns the selection and method ("model" or "grabcut").',
13628
13676
  inputSchema: {
13629
13677
  type: "object",
13630
13678
  properties: {
@@ -13638,20 +13686,25 @@ function createImageSelectionTools(controller) {
13638
13686
  point: { ...pointSchema, description: "A point on the object (natural image pixels). Pass this or rect." },
13639
13687
  mode: modeSchema,
13640
13688
  sampleAllLayers: { type: "boolean", description: "Read every layer (default true); false reads only the active pixel layer." },
13641
- enhanceEdge: { type: "boolean", description: "Smoother, less blocky edges (Auto-Enhance; default false)." }
13689
+ enhanceEdge: { type: "boolean", description: "Smoother, less blocky edges (Auto-Enhance; default false)." },
13690
+ useModel: { type: "boolean", description: "Use the host segmentation model when there is one (default true); false always uses GrabCut." }
13642
13691
  },
13643
13692
  additionalProperties: false
13644
13693
  },
13645
- annotations: writeAnnotations("Object Selection"),
13694
+ annotations: { ...writeAnnotations("Object Selection"), openWorldHint: true },
13646
13695
  handler: async (args) => {
13647
13696
  const rect = args.rect;
13648
13697
  const point = args.point;
13649
13698
  if (!rect && !point) throw new Error("Pass rect (a box around the object) or point (a point on it).");
13650
13699
  if (rect && point) throw new Error("Pass rect or point, not both.");
13651
- const options = { mode: parseSelectionMode(args.mode), sampleAllLayers: args.sampleAllLayers, enhanceEdge: args.enhanceEdge };
13652
- if (rect) await controller.selectObject({ rect: { x: num2(rect.x, NaN), y: num2(rect.y, NaN), width: num2(rect.width, NaN), height: num2(rect.height, NaN) } }, options);
13653
- else await controller.selectObject({ point: { x: num2(point.x, NaN), y: num2(point.y, NaN) } }, options);
13654
- return summary(controller);
13700
+ const options = {
13701
+ mode: parseSelectionMode(args.mode),
13702
+ sampleAllLayers: args.sampleAllLayers,
13703
+ enhanceEdge: args.enhanceEdge,
13704
+ useModel: args.useModel
13705
+ };
13706
+ const { method } = rect ? await controller.selectObject({ rect: { x: num2(rect.x, NaN), y: num2(rect.y, NaN), width: num2(rect.width, NaN), height: num2(rect.height, NaN) } }, options) : await controller.selectObject({ point: { x: num2(point.x, NaN), y: num2(point.y, NaN) } }, options);
13707
+ return { ...summary(controller), method };
13655
13708
  }
13656
13709
  };
13657
13710
  const selectFocusAreaTool = {
@@ -13684,26 +13737,28 @@ function createImageSelectionTools(controller) {
13684
13737
  const selectSkyTool = {
13685
13738
  name: "select_sky",
13686
13739
  title: "Select the sky",
13687
- description: "Select \u203A Sky: selects the sky (blue, or bright and grey when overcast, smooth, reaching down from the top of the image, including patches seen through branches or gaps), with the edge fitted to the skyline, combined with the selection by mode. Use it to replace or recolour a sky or to adjust the land without it (invert_selection). Raise threshold to take in haze or a sunset glow, lower it if bright buildings come in. One undoable step. Returns the selection, and a note when no sky was found (the selection is then unchanged).",
13740
+ description: 'Select \u203A Sky: selects the sky, combined with the selection by mode. With a host segmentation model it asks the model; otherwise it finds it classically (blue, or bright and grey when overcast, smooth, reaching down from the top of the image, including patches seen through branches or gaps) with the edge fitted to the skyline. Use it to replace or recolour a sky or to adjust the land without it (invert_selection). Raise threshold to take in haze or a sunset glow, lower it if bright buildings come in. One undoable step. Returns the selection, method ("model" or "classical"; threshold and softness apply to classical), and a note when no sky was found (the selection is then unchanged).',
13688
13741
  inputSchema: {
13689
13742
  type: "object",
13690
13743
  properties: {
13691
13744
  threshold: { type: "number", minimum: 0, maximum: 100, description: "How sky-like a pixel must be, 0..100 (default 50): higher takes in less sky-like pixels." },
13692
13745
  softness: { type: "number", minimum: 0, maximum: 100, description: "Soften the edge over this many natural pixels (default 4; 0 for a hard edge)." },
13693
13746
  mode: modeSchema,
13694
- sampleAllLayers: { type: "boolean", description: "Read every layer (default true); false reads only the active pixel layer." }
13747
+ sampleAllLayers: { type: "boolean", description: "Read every layer (default true); false reads only the active pixel layer." },
13748
+ useModel: { type: "boolean", description: "Use the host segmentation model when there is one (default true); false finds the sky classically." }
13695
13749
  },
13696
13750
  additionalProperties: false
13697
13751
  },
13698
- annotations: writeAnnotations("Select the sky", true),
13752
+ annotations: { ...writeAnnotations("Select the sky", true), openWorldHint: true },
13699
13753
  handler: async (args) => {
13700
- const { note } = await controller.selectSky({
13754
+ const { note, method } = await controller.selectSky({
13701
13755
  threshold: args.threshold === void 0 ? void 0 : num2(args.threshold, 50),
13702
13756
  softness: args.softness === void 0 ? void 0 : num2(args.softness, 4),
13703
13757
  mode: parseSelectionMode(args.mode),
13704
- sampleAllLayers: args.sampleAllLayers
13758
+ sampleAllLayers: args.sampleAllLayers,
13759
+ useModel: args.useModel
13705
13760
  });
13706
- return { ...summary(controller), ...note ? { note } : {} };
13761
+ return { ...summary(controller), method, ...note ? { note } : {} };
13707
13762
  }
13708
13763
  };
13709
13764
  const magicWandTool = {
@@ -11,6 +11,13 @@ export interface SegmentationRequest {
11
11
  target: string;
12
12
  /** Return at most this many objects (the provider's default when absent). */
13
13
  maxSegments?: number;
14
+ /**
15
+ * Object Selection: the box (natural pixels) the wanted object is in. A box-prompted model (SAM)
16
+ * should segment the object inside it; others can read `target`, which describes it too.
17
+ */
18
+ box?: CropBox;
19
+ /** Object Selection: a point (natural pixels) on the wanted object, for a point-prompted model. */
20
+ point?: ImagePoint;
14
21
  signal?: AbortSignal;
15
22
  }
16
23
  /**
@@ -11,6 +11,13 @@ export interface SegmentationRequest {
11
11
  target: string;
12
12
  /** Return at most this many objects (the provider's default when absent). */
13
13
  maxSegments?: number;
14
+ /**
15
+ * Object Selection: the box (natural pixels) the wanted object is in. A box-prompted model (SAM)
16
+ * should segment the object inside it; others can read `target`, which describes it too.
17
+ */
18
+ box?: CropBox;
19
+ /** Object Selection: a point (natural pixels) on the wanted object, for a point-prompted model. */
20
+ point?: ImagePoint;
14
21
  signal?: AbortSignal;
15
22
  }
16
23
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixerate/editor",
3
- "version": "0.202.0",
3
+ "version": "0.203.0",
4
4
  "description": "UI-agnostic rich text and media editing core library",
5
5
  "author": "Pixerate",
6
6
  "license": "MIT",
@@ -253,9 +253,9 @@ in the source frame at the document's bit depth, colour mode and profile. Read
253
253
  | `select_ellipse` | Elliptical marquee inscribed in `x`, `y`, `width`, `height`; same options. A feathered ellipse makes a vignette. Returns the summary. |
254
254
  | `select_line` | Single row or column marquee: `orientation` `row` (at y) or `column` (at x), `position` in natural px, `mode`. A 1 px line across the image. Returns the summary. |
255
255
  | `quick_select` | Quick Selection: `strokes` (brush paths of `{x, y}`), `size` (diameter, default 30), `mode`, `sampleAllLayers`, `enhanceEdge`. Each stroke grows out to its region's edges; with `replace` the first replaces and the rest add. Paint inside the area, then check and fix with `add` / `subtract` strokes. Returns the summary. |
256
- | `select_object` | Object Selection (GrabCut): `rect` (a box around the object) or `point` (on it), `mode`, `sampleAllLayers`, `enhanceEdge`. Outside the box is background. Make the box a little larger than the object. Returns the summary. |
256
+ | `select_object` | Object Selection: `rect` (a box around the object) or `point` (on it), `mode`, `sampleAllLayers`, `enhanceEdge`, `useModel`. Uses the host model when set (else, or when nothing fits, GrabCut; outside the box is background). Make the box a little larger than the object. Returns the summary and `method`. |
257
257
  | `select_focus_area` | Select › Focus Area: the in-focus (sharp) parts, edge fitted to the image. `range` 0..100 (higher takes in softer areas), `noise` 0..100 (raise when grain is selected), `softness` px, `mode`. Returns the summary and `noiseLevel`. |
258
- | `select_sky` | Select › Sky: the sky (blue or overcast, from the top down, with patches through gaps), edge fitted to the skyline. `threshold` 0..100 (higher takes in haze), `softness` px, `mode`. Returns the summary, and `note` when none was found. |
258
+ | `select_sky` | Select › Sky: the sky, from the host model when set, else classically (blue or overcast, from the top down, with patches through gaps). `threshold` 0..100 (higher takes in haze) and `softness` px for classical, `mode`, `useModel`. Returns the summary, `method`, and `note` when none was found. |
259
259
  | `select_polygon` | The lasso: `points` (at least 3 `{x, y}` along the outline, closed automatically), `mode`, `feather`, `antiAlias`; `snapToEdges: {width, contrast}` makes it the Magnetic Lasso (points snap to edges, the outline follows them). Trace a subject's contour, not its box. Returns the summary (plus `outlinePoints` when snapped). |
260
260
  | `magic_wand` | Select similar colour from `x`, `y`: `tolerance` 0..255 (default 32), `contiguous` (default true), `distance` `rgb` or `perceptual`, `sampleMerged`, `sample` (`composite` or `active-layer`), `mode`, `feather`. For flat backdrops and solid colours. Returns the summary. |
261
261
  | `select_color_range` | Color Range, soft: `samples` (`[{color, x?, y?}]`) or a `preset` (reds … magentas, highlights, midtones, shadows, skinTones); `fuzziness` (samples 0..200, default 40; tone presets' falloff, default 20), `localized` px radius around positioned samples, `invert`, `sample`, `mode`, `feather`. Returns the summary. |