@pixerate/editor 0.211.0 → 0.212.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/content-aware-fill-session.d.cts +17 -5
- package/dist/image-editor/content-aware-fill-session.d.ts +17 -5
- package/dist/image-editor/controller.d.cts +9 -4
- package/dist/image-editor/controller.d.ts +9 -4
- package/dist/image-editor/index.cjs +64 -14
- package/dist/image-editor/index.js +59 -9
- package/package.json +1 -1
- package/skills/pixerate-image-editor/SKILL.md +1 -1
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* applies it as one undo step through `contentAwareFillAsync` (the same fill the
|
|
6
6
|
* `content_aware_fill` tool makes, with a sampling area).
|
|
7
7
|
*/
|
|
8
|
-
import type { ImageEditorController } from './controller.cjs';
|
|
8
|
+
import type { ContentAwareFillOutput, ImageEditorController } from './controller.cjs';
|
|
9
9
|
import type { CropBox, ImagePoint } from './types.cjs';
|
|
10
10
|
import { type SelectionMask } from './selection/mask.cjs';
|
|
11
11
|
export type ContentAwareFillSampling = 'auto' | 'rectangular' | 'custom';
|
|
@@ -32,7 +32,7 @@ export interface ContentAwareFillSettings {
|
|
|
32
32
|
brushMode: 'add' | 'subtract';
|
|
33
33
|
/** The fill's random seed; another gives another fill to choose from. */
|
|
34
34
|
seed: number;
|
|
35
|
-
output:
|
|
35
|
+
output: ContentAwareFillOutput;
|
|
36
36
|
/** Update the preview as the area changes (otherwise with Preview). */
|
|
37
37
|
livePreview: boolean;
|
|
38
38
|
}
|
|
@@ -61,11 +61,23 @@ export declare class ContentAwareFillSession {
|
|
|
61
61
|
get window(): CropBox | null;
|
|
62
62
|
subscribe(listener: () => void): () => void;
|
|
63
63
|
private emit;
|
|
64
|
+
/** Images up to this many pixels show (and can be sampled) whole in the workspace. */
|
|
65
|
+
static readonly WHOLE_IMAGE_PIXELS = 4000000;
|
|
66
|
+
/** Room around the selection the window holds, in px each way. */
|
|
67
|
+
private margin;
|
|
64
68
|
/**
|
|
65
|
-
* Reads the layer
|
|
66
|
-
*
|
|
69
|
+
* Reads the layer and makes the first preview. The window is the whole image up to
|
|
70
|
+
* {@link WHOLE_IMAGE_PIXELS}; on a larger one it holds the selection and room around it (twice
|
|
71
|
+
* its larger side, at least 256 px, each way), and {@link expandView} widens it.
|
|
67
72
|
*/
|
|
68
73
|
open(): Promise<boolean>;
|
|
74
|
+
/** Whether the window shows the whole image (else {@link expandView} can widen it). */
|
|
75
|
+
get showsWholeImage(): boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Doubles the room around the selection (up to the whole image), keeping what was painted; a
|
|
78
|
+
* preset fills the new room as it would. Returns false when the whole image already shows.
|
|
79
|
+
*/
|
|
80
|
+
expandView(): boolean;
|
|
69
81
|
/** Sets the area to a preset: Auto (a band around the selection), Rectangular (the whole window). */
|
|
70
82
|
private applySampling;
|
|
71
83
|
/** Changes settings; a sampling preset replaces the area, and the preview follows when live. */
|
|
@@ -104,6 +116,6 @@ export declare class ContentAwareFillSession {
|
|
|
104
116
|
}): Promise<{
|
|
105
117
|
layerId: string;
|
|
106
118
|
bounds: CropBox;
|
|
107
|
-
output:
|
|
119
|
+
output: ContentAwareFillOutput;
|
|
108
120
|
}>;
|
|
109
121
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* applies it as one undo step through `contentAwareFillAsync` (the same fill the
|
|
6
6
|
* `content_aware_fill` tool makes, with a sampling area).
|
|
7
7
|
*/
|
|
8
|
-
import type { ImageEditorController } from './controller.js';
|
|
8
|
+
import type { ContentAwareFillOutput, ImageEditorController } from './controller.js';
|
|
9
9
|
import type { CropBox, ImagePoint } from './types.js';
|
|
10
10
|
import { type SelectionMask } from './selection/mask.js';
|
|
11
11
|
export type ContentAwareFillSampling = 'auto' | 'rectangular' | 'custom';
|
|
@@ -32,7 +32,7 @@ export interface ContentAwareFillSettings {
|
|
|
32
32
|
brushMode: 'add' | 'subtract';
|
|
33
33
|
/** The fill's random seed; another gives another fill to choose from. */
|
|
34
34
|
seed: number;
|
|
35
|
-
output:
|
|
35
|
+
output: ContentAwareFillOutput;
|
|
36
36
|
/** Update the preview as the area changes (otherwise with Preview). */
|
|
37
37
|
livePreview: boolean;
|
|
38
38
|
}
|
|
@@ -61,11 +61,23 @@ export declare class ContentAwareFillSession {
|
|
|
61
61
|
get window(): CropBox | null;
|
|
62
62
|
subscribe(listener: () => void): () => void;
|
|
63
63
|
private emit;
|
|
64
|
+
/** Images up to this many pixels show (and can be sampled) whole in the workspace. */
|
|
65
|
+
static readonly WHOLE_IMAGE_PIXELS = 4000000;
|
|
66
|
+
/** Room around the selection the window holds, in px each way. */
|
|
67
|
+
private margin;
|
|
64
68
|
/**
|
|
65
|
-
* Reads the layer
|
|
66
|
-
*
|
|
69
|
+
* Reads the layer and makes the first preview. The window is the whole image up to
|
|
70
|
+
* {@link WHOLE_IMAGE_PIXELS}; on a larger one it holds the selection and room around it (twice
|
|
71
|
+
* its larger side, at least 256 px, each way), and {@link expandView} widens it.
|
|
67
72
|
*/
|
|
68
73
|
open(): Promise<boolean>;
|
|
74
|
+
/** Whether the window shows the whole image (else {@link expandView} can widen it). */
|
|
75
|
+
get showsWholeImage(): boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Doubles the room around the selection (up to the whole image), keeping what was painted; a
|
|
78
|
+
* preset fills the new room as it would. Returns false when the whole image already shows.
|
|
79
|
+
*/
|
|
80
|
+
expandView(): boolean;
|
|
69
81
|
/** Sets the area to a preset: Auto (a band around the selection), Rectangular (the whole window). */
|
|
70
82
|
private applySampling;
|
|
71
83
|
/** Changes settings; a sampling preset replaces the area, and the preview follows when live. */
|
|
@@ -104,6 +116,6 @@ export declare class ContentAwareFillSession {
|
|
|
104
116
|
}): Promise<{
|
|
105
117
|
layerId: string;
|
|
106
118
|
bounds: CropBox;
|
|
107
|
-
output:
|
|
119
|
+
output: ContentAwareFillOutput;
|
|
108
120
|
}>;
|
|
109
121
|
}
|
|
@@ -479,12 +479,17 @@ export interface StrokeSelectionOptions extends Omit<FillOptions, 'useSelection'
|
|
|
479
479
|
/** Where the band lies against the selection edge (default `outside`). */
|
|
480
480
|
location?: 'inside' | 'center' | 'outside';
|
|
481
481
|
}
|
|
482
|
+
/** Where Content-Aware Fill puts its result. */
|
|
483
|
+
export type ContentAwareFillOutput = 'current' | 'new-layer' | 'duplicate-layer';
|
|
482
484
|
/** Edit › Content-Aware Fill (see `contentAwareFill`). */
|
|
483
485
|
export interface ContentAwareFillOptions {
|
|
484
486
|
/** The pixel layer to sample and fill (default the active layer). */
|
|
485
487
|
layerId?: string;
|
|
486
|
-
/**
|
|
487
|
-
|
|
488
|
+
/**
|
|
489
|
+
* `current` (default) paints into the layer; `new-layer` puts the fill on a new layer above it;
|
|
490
|
+
* `duplicate-layer` paints it into a copy of the layer, above it.
|
|
491
|
+
*/
|
|
492
|
+
output?: ContentAwareFillOutput;
|
|
488
493
|
/** How far around the selection to sample, in pixels (default its larger side, at least 32). */
|
|
489
494
|
sampleRadius?: number;
|
|
490
495
|
/** Random seed: the same seed gives the same fill (default 0). */
|
|
@@ -1528,7 +1533,7 @@ export declare class ImageEditorController {
|
|
|
1528
1533
|
contentAwareFill(options?: ContentAwareFillOptions): {
|
|
1529
1534
|
layerId: string;
|
|
1530
1535
|
bounds: CropBox;
|
|
1531
|
-
output:
|
|
1536
|
+
output: ContentAwareFillOutput;
|
|
1532
1537
|
};
|
|
1533
1538
|
/**
|
|
1534
1539
|
* `contentAwareFill` with the synthesis in the selection worker when there is one, with
|
|
@@ -1540,7 +1545,7 @@ export declare class ImageEditorController {
|
|
|
1540
1545
|
}): Promise<{
|
|
1541
1546
|
layerId: string;
|
|
1542
1547
|
bounds: CropBox;
|
|
1543
|
-
output:
|
|
1548
|
+
output: ContentAwareFillOutput;
|
|
1544
1549
|
}>;
|
|
1545
1550
|
/**
|
|
1546
1551
|
* The fill without changing anything: the window, its pixels before and after (RGBA, the fill
|
|
@@ -479,12 +479,17 @@ export interface StrokeSelectionOptions extends Omit<FillOptions, 'useSelection'
|
|
|
479
479
|
/** Where the band lies against the selection edge (default `outside`). */
|
|
480
480
|
location?: 'inside' | 'center' | 'outside';
|
|
481
481
|
}
|
|
482
|
+
/** Where Content-Aware Fill puts its result. */
|
|
483
|
+
export type ContentAwareFillOutput = 'current' | 'new-layer' | 'duplicate-layer';
|
|
482
484
|
/** Edit › Content-Aware Fill (see `contentAwareFill`). */
|
|
483
485
|
export interface ContentAwareFillOptions {
|
|
484
486
|
/** The pixel layer to sample and fill (default the active layer). */
|
|
485
487
|
layerId?: string;
|
|
486
|
-
/**
|
|
487
|
-
|
|
488
|
+
/**
|
|
489
|
+
* `current` (default) paints into the layer; `new-layer` puts the fill on a new layer above it;
|
|
490
|
+
* `duplicate-layer` paints it into a copy of the layer, above it.
|
|
491
|
+
*/
|
|
492
|
+
output?: ContentAwareFillOutput;
|
|
488
493
|
/** How far around the selection to sample, in pixels (default its larger side, at least 32). */
|
|
489
494
|
sampleRadius?: number;
|
|
490
495
|
/** Random seed: the same seed gives the same fill (default 0). */
|
|
@@ -1528,7 +1533,7 @@ export declare class ImageEditorController {
|
|
|
1528
1533
|
contentAwareFill(options?: ContentAwareFillOptions): {
|
|
1529
1534
|
layerId: string;
|
|
1530
1535
|
bounds: CropBox;
|
|
1531
|
-
output:
|
|
1536
|
+
output: ContentAwareFillOutput;
|
|
1532
1537
|
};
|
|
1533
1538
|
/**
|
|
1534
1539
|
* `contentAwareFill` with the synthesis in the selection worker when there is one, with
|
|
@@ -1540,7 +1545,7 @@ export declare class ImageEditorController {
|
|
|
1540
1545
|
}): Promise<{
|
|
1541
1546
|
layerId: string;
|
|
1542
1547
|
bounds: CropBox;
|
|
1543
|
-
output:
|
|
1548
|
+
output: ContentAwareFillOutput;
|
|
1544
1549
|
}>;
|
|
1545
1550
|
/**
|
|
1546
1551
|
* The fill without changing anything: the window, its pixels before and after (RGBA, the fill
|
|
@@ -8139,7 +8139,9 @@ var ImageEditorController = (_class12 = class {
|
|
|
8139
8139
|
if (!_optionalChain([sel, 'optionalAccess', _322 => _322.bounds])) throw new (0, _chunkH65IBXFIcjs.LayerOperationError)("Nothing is selected; select what to remove, then fill it.");
|
|
8140
8140
|
if (this.bitDepth !== 8) throw new (0, _chunkH65IBXFIcjs.LayerOperationError)("Content-Aware Fill works on 8-bit documents for now; set_bit_depth 8 first, or use spot_heal.");
|
|
8141
8141
|
const output = _nullishCoalesce(options.output, () => ( "current"));
|
|
8142
|
-
if (output !== "current" && output !== "new-layer"
|
|
8142
|
+
if (output !== "current" && output !== "new-layer" && output !== "duplicate-layer") {
|
|
8143
|
+
throw new (0, _chunkH65IBXFIcjs.LayerOperationError)(`Unknown output "${String(output)}"; use current, new-layer or duplicate-layer.`);
|
|
8144
|
+
}
|
|
8143
8145
|
const target = this.paintTarget(options.layerId, "pixels", "fill");
|
|
8144
8146
|
const { width, height } = this.naturalSize;
|
|
8145
8147
|
const b = sel.bounds;
|
|
@@ -8216,6 +8218,13 @@ var ImageEditorController = (_class12 = class {
|
|
|
8216
8218
|
}
|
|
8217
8219
|
const layerRect = { x: win.x - target.offset.x, y: win.y - target.offset.y, width: win.width, height: win.height };
|
|
8218
8220
|
const surface = _chunk474IQG74cjs.writeRegion.call(void 0, target.surface, layerRect, out);
|
|
8221
|
+
if (output === "duplicate-layer") {
|
|
8222
|
+
return this.asOneStep(() => {
|
|
8223
|
+
const copy = this.duplicateLayer(target.id);
|
|
8224
|
+
this.commitLayers(_chunkUOGKSVRQcjs.mapLayer.call(void 0, this.state.layers, copy, (l) => this.withSurface(l, "pixels", surface)));
|
|
8225
|
+
return { layerId: copy, bounds: { ...b }, output };
|
|
8226
|
+
});
|
|
8227
|
+
}
|
|
8219
8228
|
this.commitLayers(_chunkUOGKSVRQcjs.mapLayer.call(void 0, this.state.layers, target.id, (l) => this.withSurface(l, "pixels", surface)));
|
|
8220
8229
|
return { layerId: target.id, bounds: { ...b }, output };
|
|
8221
8230
|
}
|
|
@@ -13075,7 +13084,7 @@ function createImageSelectionTools(controller) {
|
|
|
13075
13084
|
inputSchema: {
|
|
13076
13085
|
type: "object",
|
|
13077
13086
|
properties: {
|
|
13078
|
-
output: { type: "string", enum: ["new-layer", "current"], description: "new-layer
|
|
13087
|
+
output: { type: "string", enum: ["new-layer", "current", "duplicate-layer"], description: "new-layer (the fill alone on a layer above), current (into the layer) or duplicate-layer (into a copy of the layer, above it); default current." },
|
|
13079
13088
|
sampleRadius: { type: "number", minimum: 32, maximum: 4e3, description: "Sample within this many pixels of the selection (default its larger side)." },
|
|
13080
13089
|
seed: { type: "integer", minimum: 0, description: "Random seed; another seed gives another fill to choose from (default 0)." },
|
|
13081
13090
|
sampleRects: { type: "array", minItems: 1, items: rectSchemaStrict, description: "Sample only from these rectangles (natural image pixels), instead of the ring around the selection." },
|
|
@@ -20188,8 +20197,8 @@ var DEFAULT_CONTENT_AWARE_FILL_SETTINGS = {
|
|
|
20188
20197
|
output: "new-layer",
|
|
20189
20198
|
livePreview: true
|
|
20190
20199
|
};
|
|
20191
|
-
var ContentAwareFillSession = (_class17 = class {
|
|
20192
|
-
constructor(controller) {;_class17.prototype.__init149.call(this);_class17.prototype.__init150.call(this);_class17.prototype.__init151.call(this);_class17.prototype.__init152.call(this);_class17.prototype.__init153.call(this);_class17.prototype.__init154.call(this);_class17.prototype.__init155.call(this);_class17.prototype.__init156.call(this);_class17.prototype.__init157.call(this);_class17.prototype.__init158.call(this);_class17.prototype.__init159.call(this);_class17.prototype.__init160.call(this);_class17.prototype.__init161.call(this);_class17.prototype.__init162.call(this);
|
|
20200
|
+
var ContentAwareFillSession = (_class17 = class _ContentAwareFillSession {
|
|
20201
|
+
constructor(controller) {;_class17.prototype.__init149.call(this);_class17.prototype.__init150.call(this);_class17.prototype.__init151.call(this);_class17.prototype.__init152.call(this);_class17.prototype.__init153.call(this);_class17.prototype.__init154.call(this);_class17.prototype.__init155.call(this);_class17.prototype.__init156.call(this);_class17.prototype.__init157.call(this);_class17.prototype.__init158.call(this);_class17.prototype.__init159.call(this);_class17.prototype.__init160.call(this);_class17.prototype.__init161.call(this);_class17.prototype.__init162.call(this);_class17.prototype.__init163.call(this);
|
|
20193
20202
|
this.controller = controller;
|
|
20194
20203
|
}
|
|
20195
20204
|
|
|
@@ -20224,16 +20233,21 @@ var ContentAwareFillSession = (_class17 = class {
|
|
|
20224
20233
|
this.revision++;
|
|
20225
20234
|
for (const l of this.listeners) l();
|
|
20226
20235
|
}
|
|
20236
|
+
/** Images up to this many pixels show (and can be sampled) whole in the workspace. */
|
|
20237
|
+
static __initStatic() {this.WHOLE_IMAGE_PIXELS = 4e6}
|
|
20238
|
+
/** Room around the selection the window holds, in px each way. */
|
|
20239
|
+
__init162() {this.margin = 0}
|
|
20227
20240
|
/**
|
|
20228
|
-
* Reads the layer
|
|
20229
|
-
*
|
|
20241
|
+
* Reads the layer and makes the first preview. The window is the whole image up to
|
|
20242
|
+
* {@link WHOLE_IMAGE_PIXELS}; on a larger one it holds the selection and room around it (twice
|
|
20243
|
+
* its larger side, at least 256 px, each way), and {@link expandView} widens it.
|
|
20230
20244
|
*/
|
|
20231
20245
|
async open() {
|
|
20232
20246
|
try {
|
|
20233
|
-
const
|
|
20234
|
-
const b = _optionalChain([
|
|
20235
|
-
|
|
20236
|
-
this.plan = this.controller.prepareContentAwareFill({ sampleRadius: margin });
|
|
20247
|
+
const { width, height } = this.controller.getState().imageDimensions;
|
|
20248
|
+
const b = _optionalChain([this, 'access', _682 => _682.controller, 'access', _683 => _683.getSelection, 'call', _684 => _684(), 'optionalAccess', _685 => _685.bounds]);
|
|
20249
|
+
this.margin = width * height <= _ContentAwareFillSession.WHOLE_IMAGE_PIXELS ? Math.max(width, height) : Math.max(256, 2 * Math.max(_nullishCoalesce(_optionalChain([b, 'optionalAccess', _686 => _686.width]), () => ( 0)), _nullishCoalesce(_optionalChain([b, 'optionalAccess', _687 => _687.height]), () => ( 0))));
|
|
20250
|
+
this.plan = this.controller.prepareContentAwareFill({ sampleRadius: this.margin });
|
|
20237
20251
|
} catch (err) {
|
|
20238
20252
|
this.error = err instanceof Error ? err.message : String(err);
|
|
20239
20253
|
this.emit();
|
|
@@ -20247,6 +20261,42 @@ var ContentAwareFillSession = (_class17 = class {
|
|
|
20247
20261
|
await this.refresh();
|
|
20248
20262
|
return true;
|
|
20249
20263
|
}
|
|
20264
|
+
/** Whether the window shows the whole image (else {@link expandView} can widen it). */
|
|
20265
|
+
get showsWholeImage() {
|
|
20266
|
+
const { width, height } = this.controller.getState().imageDimensions;
|
|
20267
|
+
const w = this.win;
|
|
20268
|
+
return !w || w.x === 0 && w.y === 0 && w.width >= width && w.height >= height;
|
|
20269
|
+
}
|
|
20270
|
+
/**
|
|
20271
|
+
* Doubles the room around the selection (up to the whole image), keeping what was painted; a
|
|
20272
|
+
* preset fills the new room as it would. Returns false when the whole image already shows.
|
|
20273
|
+
*/
|
|
20274
|
+
expandView() {
|
|
20275
|
+
if (!this.win || this.showsWholeImage) return false;
|
|
20276
|
+
const { width, height } = this.controller.getState().imageDimensions;
|
|
20277
|
+
const old = { win: this.win, area: this.area };
|
|
20278
|
+
this.margin = Math.min(Math.max(width, height), this.margin * 2);
|
|
20279
|
+
const plan = this.controller.prepareContentAwareFill({ layerId: this.plan.layerId, sampleRadius: this.margin });
|
|
20280
|
+
this.plan = plan;
|
|
20281
|
+
this.win = plan.window;
|
|
20282
|
+
this.before = plan.before;
|
|
20283
|
+
this.hole = plan.hole;
|
|
20284
|
+
const mode = this.settingsValue.sampling;
|
|
20285
|
+
this.area = null;
|
|
20286
|
+
this.applySampling(mode === "custom" ? "auto" : mode);
|
|
20287
|
+
if (mode === "custom") {
|
|
20288
|
+
const area = new Uint8Array(plan.window.width * plan.window.height);
|
|
20289
|
+
for (let y = 0; y < old.win.height; y++) {
|
|
20290
|
+
const src = y * old.win.width;
|
|
20291
|
+
const dst = (old.win.y - plan.window.y + y) * plan.window.width + (old.win.x - plan.window.x);
|
|
20292
|
+
area.set(old.area.subarray(src, src + old.win.width), dst);
|
|
20293
|
+
}
|
|
20294
|
+
this.area = area;
|
|
20295
|
+
}
|
|
20296
|
+
this.emit();
|
|
20297
|
+
if (this.settingsValue.livePreview) void this.refresh();
|
|
20298
|
+
return true;
|
|
20299
|
+
}
|
|
20250
20300
|
/** Sets the area to a preset: Auto (a band around the selection), Rectangular (the whole window). */
|
|
20251
20301
|
applySampling(mode) {
|
|
20252
20302
|
const win = this.win;
|
|
@@ -20328,7 +20378,7 @@ var ContentAwareFillSession = (_class17 = class {
|
|
|
20328
20378
|
this.emit();
|
|
20329
20379
|
return true;
|
|
20330
20380
|
}
|
|
20331
|
-
|
|
20381
|
+
__init163() {this.strokeAdds = true}
|
|
20332
20382
|
move(point) {
|
|
20333
20383
|
if (!this.lastPoint) return;
|
|
20334
20384
|
this.paint(this.lastPoint, point, this.strokeAdds);
|
|
@@ -20354,7 +20404,7 @@ var ContentAwareFillSession = (_class17 = class {
|
|
|
20354
20404
|
this.dirty = false;
|
|
20355
20405
|
try {
|
|
20356
20406
|
const area = this.samplingMask();
|
|
20357
|
-
if (!_optionalChain([area, 'optionalAccess',
|
|
20407
|
+
if (!_optionalChain([area, 'optionalAccess', _688 => _688.bounds])) throw new Error("The sampling area is empty; paint where the fill may copy from.");
|
|
20358
20408
|
const { plan, after } = await this.controller.previewContentAwareFill({ layerId: this.plan.layerId, samplingArea: area, seed: this.settingsValue.seed });
|
|
20359
20409
|
const win = this.win;
|
|
20360
20410
|
const out = Uint8ClampedArray.from(this.before);
|
|
@@ -20433,10 +20483,10 @@ var ContentAwareFillSession = (_class17 = class {
|
|
|
20433
20483
|
/** OK: fills with this sampling area, seed and output as one undo step. */
|
|
20434
20484
|
async commit(options = {}) {
|
|
20435
20485
|
const area = this.samplingMask();
|
|
20436
|
-
if (!this.plan || !_optionalChain([area, 'optionalAccess',
|
|
20486
|
+
if (!this.plan || !_optionalChain([area, 'optionalAccess', _689 => _689.bounds])) throw new Error(_nullishCoalesce(this.error, () => ( "The sampling area is empty; paint where the fill may copy from.")));
|
|
20437
20487
|
return this.controller.contentAwareFillAsync({ layerId: this.plan.layerId, samplingArea: area, seed: this.settingsValue.seed, output: this.settingsValue.output, signal: options.signal });
|
|
20438
20488
|
}
|
|
20439
|
-
}, _class17);
|
|
20489
|
+
}, _class17.__initStatic(), _class17);
|
|
20440
20490
|
|
|
20441
20491
|
|
|
20442
20492
|
|
|
@@ -8139,7 +8139,9 @@ var ImageEditorController = class {
|
|
|
8139
8139
|
if (!sel?.bounds) throw new LayerOperationError("Nothing is selected; select what to remove, then fill it.");
|
|
8140
8140
|
if (this.bitDepth !== 8) throw new LayerOperationError("Content-Aware Fill works on 8-bit documents for now; set_bit_depth 8 first, or use spot_heal.");
|
|
8141
8141
|
const output = options.output ?? "current";
|
|
8142
|
-
if (output !== "current" && output !== "new-layer"
|
|
8142
|
+
if (output !== "current" && output !== "new-layer" && output !== "duplicate-layer") {
|
|
8143
|
+
throw new LayerOperationError(`Unknown output "${String(output)}"; use current, new-layer or duplicate-layer.`);
|
|
8144
|
+
}
|
|
8143
8145
|
const target = this.paintTarget(options.layerId, "pixels", "fill");
|
|
8144
8146
|
const { width, height } = this.naturalSize;
|
|
8145
8147
|
const b = sel.bounds;
|
|
@@ -8216,6 +8218,13 @@ var ImageEditorController = class {
|
|
|
8216
8218
|
}
|
|
8217
8219
|
const layerRect = { x: win.x - target.offset.x, y: win.y - target.offset.y, width: win.width, height: win.height };
|
|
8218
8220
|
const surface = writeRegion(target.surface, layerRect, out);
|
|
8221
|
+
if (output === "duplicate-layer") {
|
|
8222
|
+
return this.asOneStep(() => {
|
|
8223
|
+
const copy = this.duplicateLayer(target.id);
|
|
8224
|
+
this.commitLayers(mapLayer(this.state.layers, copy, (l) => this.withSurface(l, "pixels", surface)));
|
|
8225
|
+
return { layerId: copy, bounds: { ...b }, output };
|
|
8226
|
+
});
|
|
8227
|
+
}
|
|
8219
8228
|
this.commitLayers(mapLayer(this.state.layers, target.id, (l) => this.withSurface(l, "pixels", surface)));
|
|
8220
8229
|
return { layerId: target.id, bounds: { ...b }, output };
|
|
8221
8230
|
}
|
|
@@ -13075,7 +13084,7 @@ function createImageSelectionTools(controller) {
|
|
|
13075
13084
|
inputSchema: {
|
|
13076
13085
|
type: "object",
|
|
13077
13086
|
properties: {
|
|
13078
|
-
output: { type: "string", enum: ["new-layer", "current"], description: "new-layer
|
|
13087
|
+
output: { type: "string", enum: ["new-layer", "current", "duplicate-layer"], description: "new-layer (the fill alone on a layer above), current (into the layer) or duplicate-layer (into a copy of the layer, above it); default current." },
|
|
13079
13088
|
sampleRadius: { type: "number", minimum: 32, maximum: 4e3, description: "Sample within this many pixels of the selection (default its larger side)." },
|
|
13080
13089
|
seed: { type: "integer", minimum: 0, description: "Random seed; another seed gives another fill to choose from (default 0)." },
|
|
13081
13090
|
sampleRects: { type: "array", minItems: 1, items: rectSchemaStrict, description: "Sample only from these rectangles (natural image pixels), instead of the ring around the selection." },
|
|
@@ -20188,7 +20197,7 @@ var DEFAULT_CONTENT_AWARE_FILL_SETTINGS = {
|
|
|
20188
20197
|
output: "new-layer",
|
|
20189
20198
|
livePreview: true
|
|
20190
20199
|
};
|
|
20191
|
-
var ContentAwareFillSession = class {
|
|
20200
|
+
var ContentAwareFillSession = class _ContentAwareFillSession {
|
|
20192
20201
|
constructor(controller) {
|
|
20193
20202
|
this.controller = controller;
|
|
20194
20203
|
}
|
|
@@ -20224,16 +20233,21 @@ var ContentAwareFillSession = class {
|
|
|
20224
20233
|
this.revision++;
|
|
20225
20234
|
for (const l of this.listeners) l();
|
|
20226
20235
|
}
|
|
20236
|
+
/** Images up to this many pixels show (and can be sampled) whole in the workspace. */
|
|
20237
|
+
static WHOLE_IMAGE_PIXELS = 4e6;
|
|
20238
|
+
/** Room around the selection the window holds, in px each way. */
|
|
20239
|
+
margin = 0;
|
|
20227
20240
|
/**
|
|
20228
|
-
* Reads the layer
|
|
20229
|
-
*
|
|
20241
|
+
* Reads the layer and makes the first preview. The window is the whole image up to
|
|
20242
|
+
* {@link WHOLE_IMAGE_PIXELS}; on a larger one it holds the selection and room around it (twice
|
|
20243
|
+
* its larger side, at least 256 px, each way), and {@link expandView} widens it.
|
|
20230
20244
|
*/
|
|
20231
20245
|
async open() {
|
|
20232
20246
|
try {
|
|
20233
|
-
const
|
|
20234
|
-
const b =
|
|
20235
|
-
|
|
20236
|
-
this.plan = this.controller.prepareContentAwareFill({ sampleRadius: margin });
|
|
20247
|
+
const { width, height } = this.controller.getState().imageDimensions;
|
|
20248
|
+
const b = this.controller.getSelection()?.bounds;
|
|
20249
|
+
this.margin = width * height <= _ContentAwareFillSession.WHOLE_IMAGE_PIXELS ? Math.max(width, height) : Math.max(256, 2 * Math.max(b?.width ?? 0, b?.height ?? 0));
|
|
20250
|
+
this.plan = this.controller.prepareContentAwareFill({ sampleRadius: this.margin });
|
|
20237
20251
|
} catch (err) {
|
|
20238
20252
|
this.error = err instanceof Error ? err.message : String(err);
|
|
20239
20253
|
this.emit();
|
|
@@ -20247,6 +20261,42 @@ var ContentAwareFillSession = class {
|
|
|
20247
20261
|
await this.refresh();
|
|
20248
20262
|
return true;
|
|
20249
20263
|
}
|
|
20264
|
+
/** Whether the window shows the whole image (else {@link expandView} can widen it). */
|
|
20265
|
+
get showsWholeImage() {
|
|
20266
|
+
const { width, height } = this.controller.getState().imageDimensions;
|
|
20267
|
+
const w = this.win;
|
|
20268
|
+
return !w || w.x === 0 && w.y === 0 && w.width >= width && w.height >= height;
|
|
20269
|
+
}
|
|
20270
|
+
/**
|
|
20271
|
+
* Doubles the room around the selection (up to the whole image), keeping what was painted; a
|
|
20272
|
+
* preset fills the new room as it would. Returns false when the whole image already shows.
|
|
20273
|
+
*/
|
|
20274
|
+
expandView() {
|
|
20275
|
+
if (!this.win || this.showsWholeImage) return false;
|
|
20276
|
+
const { width, height } = this.controller.getState().imageDimensions;
|
|
20277
|
+
const old = { win: this.win, area: this.area };
|
|
20278
|
+
this.margin = Math.min(Math.max(width, height), this.margin * 2);
|
|
20279
|
+
const plan = this.controller.prepareContentAwareFill({ layerId: this.plan.layerId, sampleRadius: this.margin });
|
|
20280
|
+
this.plan = plan;
|
|
20281
|
+
this.win = plan.window;
|
|
20282
|
+
this.before = plan.before;
|
|
20283
|
+
this.hole = plan.hole;
|
|
20284
|
+
const mode = this.settingsValue.sampling;
|
|
20285
|
+
this.area = null;
|
|
20286
|
+
this.applySampling(mode === "custom" ? "auto" : mode);
|
|
20287
|
+
if (mode === "custom") {
|
|
20288
|
+
const area = new Uint8Array(plan.window.width * plan.window.height);
|
|
20289
|
+
for (let y = 0; y < old.win.height; y++) {
|
|
20290
|
+
const src = y * old.win.width;
|
|
20291
|
+
const dst = (old.win.y - plan.window.y + y) * plan.window.width + (old.win.x - plan.window.x);
|
|
20292
|
+
area.set(old.area.subarray(src, src + old.win.width), dst);
|
|
20293
|
+
}
|
|
20294
|
+
this.area = area;
|
|
20295
|
+
}
|
|
20296
|
+
this.emit();
|
|
20297
|
+
if (this.settingsValue.livePreview) void this.refresh();
|
|
20298
|
+
return true;
|
|
20299
|
+
}
|
|
20250
20300
|
/** Sets the area to a preset: Auto (a band around the selection), Rectangular (the whole window). */
|
|
20251
20301
|
applySampling(mode) {
|
|
20252
20302
|
const win = this.win;
|
package/package.json
CHANGED
|
@@ -384,7 +384,7 @@ in the source frame at the document's bit depth, colour mode and profile. Read
|
|
|
384
384
|
| `apply_selection_as_mask` | Mask a layer with the current selection: pass `layerId` to mask an existing layer, or `kind` (plus optional `params`) to create a new masked layer in one step. `density` 0..1 and `feather` px soften the mask non-destructively. Returns `layerId`, kind, mask info and the selection summary. |
|
|
385
385
|
| `stroke_selection` | Edit › Stroke: a band `width` px (≤ 250) `outside` (default), `inside` or `center` on the selection edge, with `color`, `opacity`, `blend`, on a layer's pixels or mask (`channel`). One undo step. Returns the changed `bounds`. |
|
|
386
386
|
| `crop_to_selection` | Set the crop box to the selection's bounds (non-destructive; `reset_crop` undoes it). Returns the `crop`. |
|
|
387
|
-
| `content_aware_fill` | Remove what is selected by synthesising texture from around it on a pixel layer; `output` `new-layer` (keeps the fill separate, recommended) or `
|
|
387
|
+
| `content_aware_fill` | Remove what is selected by synthesising texture from around it on a pixel layer; `output` `new-layer` (keeps the fill separate, recommended), `current` or `duplicate-layer` (a filled copy); `sampleRadius`, `seed` (another seed, another fill); the sampling area with `sampleRects`, `samplingChannelId` and `excludeRects` (never copy from these). Select with a 2–4 px margin. 8-bit only. Returns `layerId` and `bounds`. |
|
|
388
388
|
| `copy_pixels` | Copy the selected pixels of a layer (or with `merged` the visible image) to the editor clipboard, cropped to the selection with its soft edge. No undo step. Returns `width`, `height`, `x`, `y`. |
|
|
389
389
|
| `cut_pixels` | Copy, then clear the selected pixels (one undo step). Returns the size and position. |
|
|
390
390
|
| `paste_pixels` | Paste the clipboard as a new layer: `mode` `new-layer` (centred on the selection or canvas), `in-place`, `into` (masked by the selection) or `outside` (masked by its inverse). One undo step. Returns `layerId`, `bounds`, `masked`. |
|