@pixerate/editor 0.208.0 → 0.209.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.
- package/dist/image-editor/controller.d.cts +25 -2
- package/dist/image-editor/controller.d.ts +25 -2
- package/dist/image-editor/index.cjs +405 -322
- package/dist/image-editor/index.js +96 -13
- package/dist/image-editor/selection/tool-options.d.cts +3 -1
- package/dist/image-editor/selection/tool-options.d.ts +3 -1
- package/dist/image-editor/types.d.cts +14 -0
- package/dist/image-editor/types.d.ts +14 -0
- package/package.json +1 -1
- package/skills/pixerate-image-editor/SKILL.md +1 -1
|
@@ -5103,6 +5103,7 @@ var ImageEditorController = class {
|
|
|
5103
5103
|
/** The segmentation provider behind `selectSubject` (see `setSegmentationProvider`). */
|
|
5104
5104
|
segmentation = null;
|
|
5105
5105
|
selectionWorker = null;
|
|
5106
|
+
documentOpener = null;
|
|
5106
5107
|
/** The effects provider slot (see `setEffectsProvider`). */
|
|
5107
5108
|
effects = new EffectsSlot();
|
|
5108
5109
|
/** Renders provider effects: effect smart filter results and effect layers. */
|
|
@@ -5114,6 +5115,7 @@ var ImageEditorController = class {
|
|
|
5114
5115
|
if (options.effects) this.effects.set(options.effects);
|
|
5115
5116
|
if (options.segmentation) this.segmentation = options.segmentation;
|
|
5116
5117
|
if (options.selectionWorker) this.selectionWorker = new SelectionWorkerClient(options.selectionWorker);
|
|
5118
|
+
if (options.openDocument) this.documentOpener = options.openDocument;
|
|
5117
5119
|
this.maxHistoryDepth = options.maxHistoryDepth ?? 50;
|
|
5118
5120
|
if (options.textShaper !== void 0) this.textShaperValue = options.textShaper;
|
|
5119
5121
|
this.maxHistoryBytes = options.maxHistoryBytes ?? 512 * 1024 * 1024;
|
|
@@ -5311,6 +5313,70 @@ var ImageEditorController = class {
|
|
|
5311
5313
|
this.selectionWorker?.terminate();
|
|
5312
5314
|
this.selectionWorker = factory ? new SelectionWorkerClient(factory) : null;
|
|
5313
5315
|
}
|
|
5316
|
+
/** Sets (or with `null` removes) how new documents are opened (see `DocumentOpener`). Not an edit. */
|
|
5317
|
+
setDocumentOpener(opener) {
|
|
5318
|
+
if (this.documentOpener === opener) return;
|
|
5319
|
+
this.documentOpener = opener;
|
|
5320
|
+
this.notify();
|
|
5321
|
+
}
|
|
5322
|
+
/** Whether the host can open new documents (the New Document outputs are available). */
|
|
5323
|
+
get canOpenDocuments() {
|
|
5324
|
+
return this.documentOpener !== null;
|
|
5325
|
+
}
|
|
5326
|
+
/**
|
|
5327
|
+
* A new one-layer document from the selection (or `options.selection`) on a pixel layer
|
|
5328
|
+
* (`layerId`, default the active one), the size of this image: the selected pixels on
|
|
5329
|
+
* transparency, or with `withMask` the whole layer masked by the selection. This document is
|
|
5330
|
+
* not changed. `decontaminate` (0..100) cleans the soft edge's colours as the new-layer
|
|
5331
|
+
* outputs do.
|
|
5332
|
+
*/
|
|
5333
|
+
documentFromSelection(options = {}) {
|
|
5334
|
+
const id = options.layerId ?? this.state.activeLayerId;
|
|
5335
|
+
if (!id) throw new LayerOperationError("No layer given and none is active; pass layerId (see list_layers).");
|
|
5336
|
+
const layer = requireLayer(this.state.layers, id).layer;
|
|
5337
|
+
if (layer.type !== "pixel") throw new LayerOperationError(`A new document needs a pixel layer; "${id}" is a ${layer.type} layer.`);
|
|
5338
|
+
const sel = options.selection ?? this.state.selection;
|
|
5339
|
+
if (!sel?.bounds) throw new LayerOperationError("Nothing is selected; select what goes in the new document first.");
|
|
5340
|
+
const { width, height } = this.naturalSize;
|
|
5341
|
+
const surface = this.surfaceOf(layer);
|
|
5342
|
+
let copy;
|
|
5343
|
+
if (options.withMask) {
|
|
5344
|
+
let mask = this.maskFromSelection(sel);
|
|
5345
|
+
if (layer.offset.x || layer.offset.y) mask = { ...mask, surface: translateSurface(mask.surface, -layer.offset.x, -layer.offset.y) };
|
|
5346
|
+
copy = { ...layer, id: newLayerId(), surface, mask };
|
|
5347
|
+
} else {
|
|
5348
|
+
const sb = sel.bounds;
|
|
5349
|
+
const src = { x: sb.x - layer.offset.x, y: sb.y - layer.offset.y, width: sb.width, height: sb.height };
|
|
5350
|
+
const px2 = readRegion(surface, src);
|
|
5351
|
+
const data = sel.data;
|
|
5352
|
+
for (let y = 0; y < sb.height; y++) {
|
|
5353
|
+
for (let x = 0; x < sb.width; x++) px2[(y * sb.width + x) * 4 + 3] = Math.round(px2[(y * sb.width + x) * 4 + 3] * (data[(sb.y + y) * width + sb.x + x] / 255));
|
|
5354
|
+
}
|
|
5355
|
+
copy = { ...layer, id: newLayerId(), surface: surfaceFromPixels({ width: src.width, height: src.height, data: px2 }, 4, src.x, src.y) };
|
|
5356
|
+
delete copy.mask;
|
|
5357
|
+
}
|
|
5358
|
+
delete copy.source;
|
|
5359
|
+
copy.visible = true;
|
|
5360
|
+
const amount = Math.min(100, Math.max(0, options.decontaminate ?? 0)) / 100;
|
|
5361
|
+
if (amount > 0) copy = { ...copy, surface: this.decontaminatedSurface(copy, layer, sel, amount, Math.max(4, options.radius ?? 0)) };
|
|
5362
|
+
const { layers, assets, smartContents } = serializeLayers([copy]);
|
|
5363
|
+
const out = {
|
|
5364
|
+
version: LAYERED_SCHEMA_VERSION,
|
|
5365
|
+
width,
|
|
5366
|
+
height,
|
|
5367
|
+
layers,
|
|
5368
|
+
assets,
|
|
5369
|
+
activeLayerId: copy.id,
|
|
5370
|
+
crop: null,
|
|
5371
|
+
transform: { ...DEFAULT_TRANSFORM },
|
|
5372
|
+
opacity: 1
|
|
5373
|
+
};
|
|
5374
|
+
if (smartContents) out.smartContents = smartContents;
|
|
5375
|
+
if (this.state.bitDepth === 16 || this.state.bitDepth === 32) out.bitDepth = this.state.bitDepth;
|
|
5376
|
+
if (this.state.colorProfile) out.colorProfile = serializeColorProfile(this.state.colorProfile);
|
|
5377
|
+
if (this.state.colorMode) out.colorMode = serializeColorMode(this.state.colorMode);
|
|
5378
|
+
return out;
|
|
5379
|
+
}
|
|
5314
5380
|
/** Whether the long selections run in a worker. */
|
|
5315
5381
|
get hasSelectionWorker() {
|
|
5316
5382
|
return this.selectionWorker !== null;
|
|
@@ -8752,8 +8818,12 @@ var ImageEditorController = class {
|
|
|
8752
8818
|
throw new Error("Object aware refinement needs a segmentation model (setSegmentationProvider); use refineMode color.");
|
|
8753
8819
|
}
|
|
8754
8820
|
const amount = Math.min(100, Math.max(0, options.decontaminate ?? 0)) / 100;
|
|
8755
|
-
const
|
|
8756
|
-
|
|
8821
|
+
const toDocument = output === "new-document" || output === "new-document-with-mask";
|
|
8822
|
+
const toLayer = output === "new-layer" || output === "new-layer-with-mask" || toDocument;
|
|
8823
|
+
if (amount > 0 && !toLayer) throw new Error("Decontaminate colors writes the cleaned colours to a new layer or document: use output new-layer, new-layer-with-mask, new-document or new-document-with-mask.");
|
|
8824
|
+
if (toDocument && !this.documentOpener) {
|
|
8825
|
+
throw new Error("This editor cannot open new documents (no document opener is set); use output new-layer or new-layer-with-mask instead.");
|
|
8826
|
+
}
|
|
8757
8827
|
let layerId;
|
|
8758
8828
|
if (output !== "selection") {
|
|
8759
8829
|
layerId = options.layerId ?? this.state.activeLayerId ?? void 0;
|
|
@@ -8778,6 +8848,12 @@ var ImageEditorController = class {
|
|
|
8778
8848
|
}
|
|
8779
8849
|
const refined = refineSelection(pixels, sel, params, edited.refineBrush);
|
|
8780
8850
|
if (!refined.bounds) throw new Error("Refining left nothing selected; use a smaller shiftEdge or contrast.");
|
|
8851
|
+
if (toDocument) {
|
|
8852
|
+
const document2 = this.documentFromSelection({ layerId, withMask: output === "new-document-with-mask", selection: refined, decontaminate: amount * 100, radius: params.radius });
|
|
8853
|
+
const name = `${requireLayer(this.state.layers, layerId).layer.name || "Layer"} ${output === "new-document" ? "cutout" : "masked"}`;
|
|
8854
|
+
const documentId = await this.documentOpener(document2, { name, from: "select-and-mask" });
|
|
8855
|
+
return { output, layerId, refineMode, ...documentId ? { documentId } : {} };
|
|
8856
|
+
}
|
|
8781
8857
|
return this.asOneStep(() => {
|
|
8782
8858
|
this.setSelection(refined);
|
|
8783
8859
|
if (output === "selection") return { output, refineMode };
|
|
@@ -8808,6 +8884,16 @@ var ImageEditorController = class {
|
|
|
8808
8884
|
const layer = requireLayer(this.state.layers, id).layer;
|
|
8809
8885
|
const source = requireLayer(this.state.layers, sourceId).layer;
|
|
8810
8886
|
if (layer.type !== "pixel" || source.type !== "pixel" || !coverage.bounds) return;
|
|
8887
|
+
const surface = this.decontaminatedSurface(layer, source, coverage, amount, radius);
|
|
8888
|
+
const next = mapLayer(this.state.layers, id, (l) => {
|
|
8889
|
+
const updated = { ...l, surface };
|
|
8890
|
+
delete updated.source;
|
|
8891
|
+
return updated;
|
|
8892
|
+
});
|
|
8893
|
+
this.commitLayers(next);
|
|
8894
|
+
}
|
|
8895
|
+
/** `layer`'s surface with Decontaminate Colors applied along `coverage`'s soft edge, colours read from `source`. */
|
|
8896
|
+
decontaminatedSurface(layer, source, coverage, amount, radius) {
|
|
8811
8897
|
const { width, height } = this.naturalSize;
|
|
8812
8898
|
const pad = Math.ceil(radius * 3);
|
|
8813
8899
|
const b = coverage.bounds;
|
|
@@ -8823,12 +8909,7 @@ var ImageEditorController = class {
|
|
|
8823
8909
|
for (let y = 0; y < box.height; y++) cov.set(all.subarray((box.y + y) * width + box.x, (box.y + y) * width + box.x + box.width), y * box.width);
|
|
8824
8910
|
const cleaned = decontaminateColors(original, cov, box.width, box.height, amount, radius);
|
|
8825
8911
|
for (let i = 3; i < cleaned.length; i += 4) cleaned[i] = rgba[i];
|
|
8826
|
-
|
|
8827
|
-
const updated = { ...l, surface: writeRegion(surface, src, cleaned) };
|
|
8828
|
-
delete updated.source;
|
|
8829
|
-
return updated;
|
|
8830
|
-
});
|
|
8831
|
-
this.commitLayers(next);
|
|
8912
|
+
return writeRegion(surface, src, cleaned);
|
|
8832
8913
|
}
|
|
8833
8914
|
/**
|
|
8834
8915
|
* Loads a selection from a mask image (an AI segmentation, a saved mask): a data URL / URL or
|
|
@@ -11805,7 +11886,9 @@ var REFINE_OUTPUTS = [
|
|
|
11805
11886
|
{ id: "selection", label: "Selection" },
|
|
11806
11887
|
{ id: "layer-mask", label: "Layer Mask" },
|
|
11807
11888
|
{ id: "new-layer", label: "New Layer" },
|
|
11808
|
-
{ id: "new-layer-with-mask", label: "New Layer with Layer Mask" }
|
|
11889
|
+
{ id: "new-layer-with-mask", label: "New Layer with Layer Mask" },
|
|
11890
|
+
{ id: "new-document", label: "New Document", needsDocuments: true },
|
|
11891
|
+
{ id: "new-document-with-mask", label: "New Document with Layer Mask", needsDocuments: true }
|
|
11809
11892
|
];
|
|
11810
11893
|
function describeSelection(mask) {
|
|
11811
11894
|
if (!mask || !mask.bounds) return "No selection";
|
|
@@ -12027,11 +12110,11 @@ function createImageSelectionTools(controller) {
|
|
|
12027
12110
|
shiftEdge: { type: "number", minimum: -100, maximum: 100, description: "Move the edge out (+) or in (-), in percent." },
|
|
12028
12111
|
output: {
|
|
12029
12112
|
type: "string",
|
|
12030
|
-
enum: ["selection", "layer-mask", "new-layer", "new-layer-with-mask"],
|
|
12031
|
-
description: "Where the result goes (default selection). layer-mask keeps every pixel and is easiest to revise; new-layer-with-mask too, on a copy."
|
|
12113
|
+
enum: ["selection", "layer-mask", "new-layer", "new-layer-with-mask", "new-document", "new-document-with-mask"],
|
|
12114
|
+
description: "Where the result goes (default selection). layer-mask keeps every pixel and is easiest to revise; new-layer-with-mask too, on a copy. new-document and new-document-with-mask hand a new one-layer document to the host (only when the host can open documents; this one is unchanged) and return its documentId."
|
|
12032
12115
|
},
|
|
12033
12116
|
layerId: { type: "string", description: "The layer for layer outputs (default the active layer; see list_layers). New-layer outputs need a pixel layer." },
|
|
12034
|
-
decontaminate: { type: "number", minimum: 0, maximum: 100, description: "Decontaminate Colors amount 0-100 (default 0), only with
|
|
12117
|
+
decontaminate: { type: "number", minimum: 0, maximum: 100, description: "Decontaminate Colors amount 0-100 (default 0), only with the new-layer and new-document outputs." },
|
|
12035
12118
|
edits: {
|
|
12036
12119
|
type: "array",
|
|
12037
12120
|
description: "Strokes applied before refining, in order.",
|
|
@@ -17877,7 +17960,7 @@ var SelectAndMaskSession = class {
|
|
|
17877
17960
|
remembered = s.remember ? { ...s, params: { ...s.params } } : null;
|
|
17878
17961
|
return this.controller.refineSelectionTo(s.params, s.output, {
|
|
17879
17962
|
edits: [...this.editsList],
|
|
17880
|
-
decontaminate: s.output === "
|
|
17963
|
+
decontaminate: s.output === "selection" || s.output === "layer-mask" ? 0 : s.decontaminate,
|
|
17881
17964
|
refineMode: s.refineMode
|
|
17882
17965
|
});
|
|
17883
17966
|
}
|
|
@@ -81,8 +81,10 @@ export interface RefineEdgeControl {
|
|
|
81
81
|
export declare const REFINE_EDGE_CONTROLS: readonly RefineEdgeControl[];
|
|
82
82
|
/** Select and Mask's outputs, in menu order. */
|
|
83
83
|
export declare const REFINE_OUTPUTS: readonly {
|
|
84
|
-
id: 'selection' | 'layer-mask' | 'new-layer' | 'new-layer-with-mask';
|
|
84
|
+
id: 'selection' | 'layer-mask' | 'new-layer' | 'new-layer-with-mask' | 'new-document' | 'new-document-with-mask';
|
|
85
85
|
label: string;
|
|
86
|
+
/** Needs a host that can open documents (`controller.canOpenDocuments`). */
|
|
87
|
+
needsDocuments?: boolean;
|
|
86
88
|
}[];
|
|
87
89
|
/** A one-line status: "No selection" or "W × H px selected (n % of the image)". */
|
|
88
90
|
export declare function describeSelection(mask: SelectionMask | null | undefined): string;
|
|
@@ -81,8 +81,10 @@ export interface RefineEdgeControl {
|
|
|
81
81
|
export declare const REFINE_EDGE_CONTROLS: readonly RefineEdgeControl[];
|
|
82
82
|
/** Select and Mask's outputs, in menu order. */
|
|
83
83
|
export declare const REFINE_OUTPUTS: readonly {
|
|
84
|
-
id: 'selection' | 'layer-mask' | 'new-layer' | 'new-layer-with-mask';
|
|
84
|
+
id: 'selection' | 'layer-mask' | 'new-layer' | 'new-layer-with-mask' | 'new-document' | 'new-document-with-mask';
|
|
85
85
|
label: string;
|
|
86
|
+
/** Needs a host that can open documents (`controller.canOpenDocuments`). */
|
|
87
|
+
needsDocuments?: boolean;
|
|
86
88
|
}[];
|
|
87
89
|
/** A one-line status: "No selection" or "W × H px selected (n % of the image)". */
|
|
88
90
|
export declare function describeSelection(mask: SelectionMask | null | undefined): string;
|
|
@@ -456,7 +456,21 @@ export interface ImageEditorOptions {
|
|
|
456
456
|
* Without one they run on the page. Change it later with `setSelectionWorker`.
|
|
457
457
|
*/
|
|
458
458
|
selectionWorker?: SelectionWorkerFactory | null;
|
|
459
|
+
/**
|
|
460
|
+
* Opens a new image document the editor made (Select and Mask's New Document outputs) the way
|
|
461
|
+
* the host shows documents: a tab, a window, a file. Without it those outputs are unavailable.
|
|
462
|
+
* Change it later with `setDocumentOpener`.
|
|
463
|
+
*/
|
|
464
|
+
openDocument?: DocumentOpener | null;
|
|
459
465
|
}
|
|
466
|
+
/**
|
|
467
|
+
* Opens a new image document for the host: `document` loads with `loadJSON`; `info.name` is a
|
|
468
|
+
* suggested name. May return the new document's id (handed back to the caller and to agents).
|
|
469
|
+
*/
|
|
470
|
+
export type DocumentOpener = (document: SerializedImageEditorState, info: {
|
|
471
|
+
name: string;
|
|
472
|
+
from: 'select-and-mask';
|
|
473
|
+
}) => string | void | Promise<string | void>;
|
|
460
474
|
export interface InpaintOptions {
|
|
461
475
|
prompt: string;
|
|
462
476
|
imageDataUrl: string;
|
|
@@ -456,7 +456,21 @@ export interface ImageEditorOptions {
|
|
|
456
456
|
* Without one they run on the page. Change it later with `setSelectionWorker`.
|
|
457
457
|
*/
|
|
458
458
|
selectionWorker?: SelectionWorkerFactory | null;
|
|
459
|
+
/**
|
|
460
|
+
* Opens a new image document the editor made (Select and Mask's New Document outputs) the way
|
|
461
|
+
* the host shows documents: a tab, a window, a file. Without it those outputs are unavailable.
|
|
462
|
+
* Change it later with `setDocumentOpener`.
|
|
463
|
+
*/
|
|
464
|
+
openDocument?: DocumentOpener | null;
|
|
459
465
|
}
|
|
466
|
+
/**
|
|
467
|
+
* Opens a new image document for the host: `document` loads with `loadJSON`; `info.name` is a
|
|
468
|
+
* suggested name. May return the new document's id (handed back to the caller and to agents).
|
|
469
|
+
*/
|
|
470
|
+
export type DocumentOpener = (document: SerializedImageEditorState, info: {
|
|
471
|
+
name: string;
|
|
472
|
+
from: 'select-and-mask';
|
|
473
|
+
}) => string | void | Promise<string | void>;
|
|
460
474
|
export interface InpaintOptions {
|
|
461
475
|
prompt: string;
|
|
462
476
|
imageDataUrl: string;
|
package/package.json
CHANGED
|
@@ -269,7 +269,7 @@ in the source frame at the document's bit depth, colour mode and profile. Read
|
|
|
269
269
|
| `grow_selection` | Select › Grow: add connected pixels within `tolerance` (default 32) of a selected colour; follows colour, not distance. `sample`, `sampleMerged`. Fails when nothing is selected. Returns the summary. |
|
|
270
270
|
| `select_similar` | Select › Similar: add every pixel in the image within `tolerance` of a selected colour, connected or not. Fails when nothing is selected. Returns the summary. |
|
|
271
271
|
| `reselect` | Bring back the last selection `deselect` cleared (one undo step). Fails when there is none. Returns the summary. |
|
|
272
|
-
| `refine_selection` | Select and Mask: snap the selection edge to image edges within `radius` px (0..64, default 8), with `smartRadius`, `smooth` 0..100, `feather` px, `contrast` 0..100 and `shiftEdge` -100..100 %, then `output` to `selection` (default), `layer-mask`, `new-layer
|
|
272
|
+
| `refine_selection` | Select and Mask: snap the selection edge to image edges within `radius` px (0..64, default 8), with `smartRadius`, `smooth` 0..100, `feather` px, `contrast` 0..100 and `shiftEdge` -100..100 %, then `output` to `selection` (default), `layer-mask`, `new-layer`, `new-layer-with-mask` (on `layerId`, default active; new layers hide the source), or `new-document` / `new-document-with-mask` (host must open documents; returns `documentId`), with `decontaminate` 0..100 for new layers and documents. `edits` (`quick-select`, `brush`, `lasso`, `object` add/subtract; `refine-brush` over hair) apply first; `refineMode: "object"` uses the host model. One step; returns `refineMode`, `layerId` / `newLayerId`. Use on hair, fur, foliage or a coarse AI mask. |
|
|
273
273
|
| `update_adjustment_layer` | Change a layer's `enabled` (false hides it without deleting), `blend`, `opacity` (0..1) or `name` in one undo step. Returns the layer. Its parameters change with `set_adjustments`. Toggle `enabled` to compare a layer's effect before and after. |
|
|
274
274
|
| `move_adjustment_layer` | Move a layer to index `to` (0 = bottom, applied first; past the top means top). Order matters: a curves layer above a hue/saturation layer works on its output. Fails when the layer is already there. Returns the ids bottom to top. |
|
|
275
275
|
| `remove_adjustment_layer` | Delete a layer and its mask. Prefer hiding it with `update_adjustment_layer` when the user may want it back. Returns the remaining ids. |
|