@pixerate/editor 0.186.0 → 0.188.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/{chunk-FW7N64XK.js → chunk-IGMNJM2H.js} +221 -20
- package/dist/{chunk-X2KQQZBT.cjs → chunk-JYV4R62M.cjs} +381 -180
- package/dist/image-editor/controller.d.cts +36 -3
- package/dist/image-editor/controller.d.ts +36 -3
- package/dist/image-editor/index.cjs +874 -718
- package/dist/image-editor/index.js +206 -50
- package/dist/image-editor/selection/index.d.cts +1 -0
- package/dist/image-editor/selection/index.d.ts +1 -0
- package/dist/image-editor/selection/mask-files.d.cts +20 -0
- package/dist/image-editor/selection/mask-files.d.ts +20 -0
- package/dist/image-editor/selection/mask.d.cts +20 -1
- package/dist/image-editor/selection/mask.d.ts +20 -1
- package/dist/image-editor/selection/shapes.d.cts +8 -0
- package/dist/image-editor/selection/shapes.d.ts +8 -0
- package/dist/image-editor/selection/tool-options.d.cts +1 -1
- package/dist/image-editor/selection/tool-options.d.ts +1 -1
- package/dist/paint-editor/index.cjs +6 -6
- package/dist/paint-editor/index.js +1 -1
- package/package.json +1 -1
- package/skills/pixerate-image-editor/SKILL.md +5 -24
- package/skills/pixerate-image-editor/references/painting.md +29 -0
- package/skills/pixerate-image-selection-masking/SKILL.md +7 -1
|
@@ -298,6 +298,8 @@ import {
|
|
|
298
298
|
selectionArea,
|
|
299
299
|
selectionFromCoverage,
|
|
300
300
|
selectionFromPixels,
|
|
301
|
+
selectionFromRegion,
|
|
302
|
+
selectionFromTiles,
|
|
301
303
|
selectionToCoverage,
|
|
302
304
|
selectionToPixels,
|
|
303
305
|
selectionValueAt,
|
|
@@ -370,7 +372,7 @@ import {
|
|
|
370
372
|
writePsd,
|
|
371
373
|
xyToTempTint,
|
|
372
374
|
xyToUv
|
|
373
|
-
} from "../chunk-
|
|
375
|
+
} from "../chunk-IGMNJM2H.js";
|
|
374
376
|
import {
|
|
375
377
|
parseCssColor
|
|
376
378
|
} from "../chunk-3CI7FRMK.js";
|
|
@@ -2835,8 +2837,33 @@ var TypeToolGesture = class {
|
|
|
2835
2837
|
|
|
2836
2838
|
// src/image-editor/selection/shapes.ts
|
|
2837
2839
|
function polygonCoverage(points, width, height, antiAlias = true) {
|
|
2840
|
+
const { box, coverage } = polygonCoverageBox(points, width, height, antiAlias);
|
|
2841
|
+
if (!box || box.width === width && box.height === height) return box ? coverage : new Float32Array(width * height);
|
|
2838
2842
|
const m = new Float32Array(width * height);
|
|
2839
|
-
|
|
2843
|
+
for (let y = 0; y < box.height; y++) m.set(coverage.subarray(y * box.width, (y + 1) * box.width), (box.y + y) * width + box.x);
|
|
2844
|
+
return m;
|
|
2845
|
+
}
|
|
2846
|
+
function polygonCoverageBox(points, width, height, antiAlias = true) {
|
|
2847
|
+
const none = { box: null, coverage: new Float32Array(0) };
|
|
2848
|
+
if (points.length < 3 || width <= 0 || height <= 0) return none;
|
|
2849
|
+
let minX = Infinity;
|
|
2850
|
+
let minY = Infinity;
|
|
2851
|
+
let maxX = -Infinity;
|
|
2852
|
+
let maxY = -Infinity;
|
|
2853
|
+
for (const p of points) {
|
|
2854
|
+
if (!Number.isFinite(p.x) || !Number.isFinite(p.y)) continue;
|
|
2855
|
+
minX = Math.min(minX, p.x);
|
|
2856
|
+
minY = Math.min(minY, p.y);
|
|
2857
|
+
maxX = Math.max(maxX, p.x);
|
|
2858
|
+
maxY = Math.max(maxY, p.y);
|
|
2859
|
+
}
|
|
2860
|
+
const bx0 = Math.max(0, Math.floor(minX));
|
|
2861
|
+
const by0 = Math.max(0, Math.floor(minY));
|
|
2862
|
+
const bx1 = Math.min(width, Math.ceil(maxX));
|
|
2863
|
+
const by1 = Math.min(height, Math.ceil(maxY));
|
|
2864
|
+
if (!(bx1 > bx0 && by1 > by0)) return none;
|
|
2865
|
+
const bw = bx1 - bx0;
|
|
2866
|
+
const m = new Float32Array(bw * (by1 - by0));
|
|
2840
2867
|
const sub2 = antiAlias ? 4 : 1;
|
|
2841
2868
|
const edges = [];
|
|
2842
2869
|
for (let i = 0; i < points.length; i++) {
|
|
@@ -2850,8 +2877,8 @@ function polygonCoverage(points, width, height, antiAlias = true) {
|
|
|
2850
2877
|
let next = 0;
|
|
2851
2878
|
let active = [];
|
|
2852
2879
|
const xs = [];
|
|
2853
|
-
for (let py =
|
|
2854
|
-
const row = py *
|
|
2880
|
+
for (let py = by0; py < by1; py++) {
|
|
2881
|
+
const row = (py - by0) * bw - bx0;
|
|
2855
2882
|
for (let s = 0; s < sub2; s++) {
|
|
2856
2883
|
const y = py + (s + 0.5) / sub2;
|
|
2857
2884
|
while (next < edges.length && edges[next].top <= y) active.push(edges[next++]);
|
|
@@ -2881,15 +2908,14 @@ function polygonCoverage(points, width, height, antiAlias = true) {
|
|
|
2881
2908
|
}
|
|
2882
2909
|
}
|
|
2883
2910
|
for (let i = 0; i < m.length; i++) if (m[i] > 1) m[i] = 1;
|
|
2884
|
-
return m;
|
|
2911
|
+
return { box: { x: bx0, y: by0, width: bw, height: by1 - by0 }, coverage: m };
|
|
2885
2912
|
}
|
|
2886
|
-
function
|
|
2913
|
+
function polygonSelection(width, height, points, options = {}) {
|
|
2914
|
+
const { box, coverage } = polygonCoverageBox(points, width, height, options.antiAlias ?? true);
|
|
2915
|
+
if (!box) return createSelectionMask(width, height);
|
|
2887
2916
|
const out = new Uint8Array(coverage.length);
|
|
2888
2917
|
for (let i = 0; i < out.length; i++) out[i] = coverageToByte(coverage[i]);
|
|
2889
|
-
return
|
|
2890
|
-
}
|
|
2891
|
-
function polygonSelection(width, height, points, options = {}) {
|
|
2892
|
-
return toMask(width, height, polygonCoverage(points, width, height, options.antiAlias ?? true));
|
|
2918
|
+
return selectionFromRegion(width, height, box, out);
|
|
2893
2919
|
}
|
|
2894
2920
|
function normalizeRect(rect) {
|
|
2895
2921
|
const x = rect.width < 0 ? rect.x + rect.width : rect.x;
|
|
@@ -3531,9 +3557,7 @@ function selectionRing(sel, width, location) {
|
|
|
3531
3557
|
var DEFAULT_CHANNEL_COLOR = "#ff0000";
|
|
3532
3558
|
function describeChannel(channel) {
|
|
3533
3559
|
const { mask, ...rest } = channel;
|
|
3534
|
-
|
|
3535
|
-
for (let i = 0; i < mask.data.length; i++) sum += mask.data[i];
|
|
3536
|
-
const percent = Math.round(sum / 255 / Math.max(1, mask.data.length) * 1e4) / 100;
|
|
3560
|
+
const percent = Math.round(selectionArea(mask) / Math.max(1, mask.width * mask.height) * 1e4) / 100;
|
|
3537
3561
|
return { ...rest, bounds: mask.bounds, percent };
|
|
3538
3562
|
}
|
|
3539
3563
|
function serializeChannels(channels) {
|
|
@@ -4144,8 +4168,7 @@ var FreeTransformSession = class {
|
|
|
4144
4168
|
const result = part.base ? compositeSurfaceOver(part.base, moved) : moved;
|
|
4145
4169
|
if (part.what === "selection") {
|
|
4146
4170
|
const { width, height } = this.host.canvas();
|
|
4147
|
-
const
|
|
4148
|
-
const next = createSelectionMask(width, height, data);
|
|
4171
|
+
const next = selectionFromTiles(width, height, result);
|
|
4149
4172
|
selection = next.bounds ? next : null;
|
|
4150
4173
|
} else if (part.what === "pixels") {
|
|
4151
4174
|
layers = mapLayer(layers, targetId, (l) => {
|
|
@@ -5471,14 +5494,18 @@ var ImageEditorController = class {
|
|
|
5471
5494
|
this.trimHistoryBytes();
|
|
5472
5495
|
}
|
|
5473
5496
|
/**
|
|
5474
|
-
* Bytes of pixel tiles held only by undo / redo steps (not by the
|
|
5475
|
-
* undo steps are dropped while it exceeds `maxHistoryBytes`.
|
|
5497
|
+
* Bytes of pixel, selection and channel tiles held only by undo / redo steps (not by the
|
|
5498
|
+
* current state). The oldest undo steps are dropped while it exceeds `maxHistoryBytes`.
|
|
5476
5499
|
*/
|
|
5477
5500
|
get historyBytes() {
|
|
5478
5501
|
const live = /* @__PURE__ */ new Set();
|
|
5479
5502
|
collectStateTiles(this.state.layers, live);
|
|
5503
|
+
collectSelectionTiles(this.state.selection, this.state.channels, live);
|
|
5480
5504
|
const held = /* @__PURE__ */ new Set();
|
|
5481
|
-
for (const e of [...this.undoStack, ...this.redoStack])
|
|
5505
|
+
for (const e of [...this.undoStack, ...this.redoStack]) {
|
|
5506
|
+
collectStateTiles(e.layers, held);
|
|
5507
|
+
collectSelectionTiles(e.selection, e.channels, held);
|
|
5508
|
+
}
|
|
5482
5509
|
let n = 0;
|
|
5483
5510
|
for (const t of held) if (!live.has(t)) n += t.data.byteLength;
|
|
5484
5511
|
return n;
|
|
@@ -7376,7 +7403,7 @@ var ImageEditorController = class {
|
|
|
7376
7403
|
/** @internal Shows a stroke's live surface (no history). */
|
|
7377
7404
|
_strokeUpdate(layerId, channel, surface, rects) {
|
|
7378
7405
|
if (channel === "quick-mask") {
|
|
7379
|
-
this.state.selection = this.selectionFromSurface(surface
|
|
7406
|
+
this.state.selection = this.selectionFromSurface(surface);
|
|
7380
7407
|
this.notify();
|
|
7381
7408
|
return;
|
|
7382
7409
|
}
|
|
@@ -8220,6 +8247,39 @@ var ImageEditorController = class {
|
|
|
8220
8247
|
this.setSelection(selectionFromPixels(pixels, options), options.mode ?? "replace");
|
|
8221
8248
|
return this.getSelection();
|
|
8222
8249
|
}
|
|
8250
|
+
/**
|
|
8251
|
+
* Loads a mask image (a data URL / URL or RGBA pixels, any size, resampled) into a new alpha
|
|
8252
|
+
* channel, read like `loadSelectionFromImage`. The selection does not change. One undo step.
|
|
8253
|
+
*/
|
|
8254
|
+
async loadChannelFromImage(source, options = {}) {
|
|
8255
|
+
const mask = selectionFromPixels(await this.decodeMaskImage(source), options);
|
|
8256
|
+
const { width, height } = this.naturalSize;
|
|
8257
|
+
return this.addChannel(mask.width === width && mask.height === height ? mask : resizeSelection(mask, width, height), options.name);
|
|
8258
|
+
}
|
|
8259
|
+
/**
|
|
8260
|
+
* A full-size greyscale PNG (white = selected / revealed) of the selection, an alpha channel or
|
|
8261
|
+
* a layer's painted mask, as a data URL; `invert` swaps black and white, `maxSide` scales it
|
|
8262
|
+
* down. Encoded in code, so it works without a canvas. Throws when the source is missing.
|
|
8263
|
+
*/
|
|
8264
|
+
exportMaskImage(source, options = {}) {
|
|
8265
|
+
let mask;
|
|
8266
|
+
if (source === "selection") {
|
|
8267
|
+
const sel = this.state.selection;
|
|
8268
|
+
if (!sel?.bounds) throw new LayerOperationError("Nothing is selected; select first, or export a channel or a layer mask.");
|
|
8269
|
+
mask = sel;
|
|
8270
|
+
} else if ("channelId" in source) {
|
|
8271
|
+
mask = this.requireChannel(source.channelId).mask;
|
|
8272
|
+
} else {
|
|
8273
|
+
const layer = requireLayer(this.state.layers, source.layerId).layer;
|
|
8274
|
+
const coverage = this.layerMaskCoverage(source.layerId);
|
|
8275
|
+
if (!coverage) throw new LayerOperationError(`Layer "${layer.name || source.layerId}" has no painted mask to export.`);
|
|
8276
|
+
mask = coverage;
|
|
8277
|
+
}
|
|
8278
|
+
const k = options.maxSide ? Math.min(1, options.maxSide / Math.max(mask.width, mask.height)) : 1;
|
|
8279
|
+
if (k < 1) mask = resizeSelection(mask, Math.max(1, Math.round(mask.width * k)), Math.max(1, Math.round(mask.height * k)));
|
|
8280
|
+
if (options.invert) mask = invertSelection(mask);
|
|
8281
|
+
return { width: mask.width, height: mask.height, dataUrl: pngDataUrl(mask.width, mask.height, mask.data, 1) };
|
|
8282
|
+
}
|
|
8223
8283
|
/** A mask image (data URL / URL, or pixels as they are) as RGBA pixels. */
|
|
8224
8284
|
async decodeMaskImage(source) {
|
|
8225
8285
|
if (typeof source !== "string") return source;
|
|
@@ -8337,10 +8397,15 @@ var ImageEditorController = class {
|
|
|
8337
8397
|
this.setChannels(channels.map((c) => c.id === existing.id ? next : c));
|
|
8338
8398
|
return next;
|
|
8339
8399
|
}
|
|
8400
|
+
return this.addChannel(sized, options.name);
|
|
8401
|
+
}
|
|
8402
|
+
/** Appends a new channel holding `mask` (canvas-sized) as one undo step. */
|
|
8403
|
+
addChannel(mask, name) {
|
|
8404
|
+
const channels = this.getChannels();
|
|
8340
8405
|
const channel = {
|
|
8341
8406
|
id: this.newChannelId(),
|
|
8342
|
-
name:
|
|
8343
|
-
mask
|
|
8407
|
+
name: name?.trim() || `Alpha ${channels.length + 1}`,
|
|
8408
|
+
mask,
|
|
8344
8409
|
color: DEFAULT_CHANNEL_COLOR,
|
|
8345
8410
|
opacity: 0.5,
|
|
8346
8411
|
indicates: "masked",
|
|
@@ -8416,35 +8481,18 @@ var ImageEditorController = class {
|
|
|
8416
8481
|
const { width, height } = this.naturalSize;
|
|
8417
8482
|
const sel = this.state.selection;
|
|
8418
8483
|
if (!sel?.bounds) return emptySurface(1, 0);
|
|
8419
|
-
|
|
8420
|
-
return surfaceFromPixels({ width, height, data: sized.data }, 1, 0, 0, 0);
|
|
8484
|
+
return (sel.width === width && sel.height === height ? sel : resizeSelection(sel, width, height)).surface;
|
|
8421
8485
|
}
|
|
8422
|
-
/** The selection a painted Quick Mask surface stands for (
|
|
8423
|
-
selectionFromSurface(surface
|
|
8486
|
+
/** The selection a painted Quick Mask surface stands for (it shares the surface's tiles). */
|
|
8487
|
+
selectionFromSurface(surface) {
|
|
8424
8488
|
const { width, height } = this.naturalSize;
|
|
8425
|
-
|
|
8426
|
-
if (base && rects && base.width === width && base.height === height) {
|
|
8427
|
-
data = new Uint8Array(base.data);
|
|
8428
|
-
for (const r of rects) {
|
|
8429
|
-
const x0 = Math.max(0, Math.floor(r.x));
|
|
8430
|
-
const y0 = Math.max(0, Math.floor(r.y));
|
|
8431
|
-
const x1 = Math.min(width, Math.ceil(r.x + r.width));
|
|
8432
|
-
const y1 = Math.min(height, Math.ceil(r.y + r.height));
|
|
8433
|
-
if (x1 <= x0 || y1 <= y0) continue;
|
|
8434
|
-
const part = readRegion(surface, { x: x0, y: y0, width: x1 - x0, height: y1 - y0 });
|
|
8435
|
-
for (let y = y0; y < y1; y++) data.set(part.subarray((y - y0) * (x1 - x0), (y - y0 + 1) * (x1 - x0)), y * width + x0);
|
|
8436
|
-
}
|
|
8437
|
-
} else {
|
|
8438
|
-
const all = readRegion(surface, { x: 0, y: 0, width, height });
|
|
8439
|
-
data = new Uint8Array(all.buffer, all.byteOffset, all.length);
|
|
8440
|
-
}
|
|
8441
|
-
return normalizeSelection(createSelectionMask(width, height, data));
|
|
8489
|
+
return normalizeSelection(selectionFromTiles(width, height, surface));
|
|
8442
8490
|
}
|
|
8443
8491
|
// --- Layer masks ---
|
|
8444
8492
|
maskFromSelection(sel, props = {}) {
|
|
8445
8493
|
const { width, height } = this.naturalSize;
|
|
8446
8494
|
const sized = sel.width === width && sel.height === height ? sel : resizeSelection(sel, width, height);
|
|
8447
|
-
const mask = { surface:
|
|
8495
|
+
const mask = { surface: sized.surface, defaultValue: 0 };
|
|
8448
8496
|
return withMaskProps(mask, props);
|
|
8449
8497
|
}
|
|
8450
8498
|
/**
|
|
@@ -8486,14 +8534,19 @@ var ImageEditorController = class {
|
|
|
8486
8534
|
* with `invert`. False without a painted mask (a depth mask is computed, not painted).
|
|
8487
8535
|
*/
|
|
8488
8536
|
selectionFromLayerMask(layerId, mode = "replace", invert2 = false) {
|
|
8537
|
+
const sel = this.layerMaskCoverage(layerId);
|
|
8538
|
+
if (!sel) return false;
|
|
8539
|
+
return this.setSelection(invert2 ? invertSelection(sel) : sel, mode);
|
|
8540
|
+
}
|
|
8541
|
+
/** A layer's painted mask over the canvas as a selection mask; null without one (or for a depth mask). */
|
|
8542
|
+
layerMaskCoverage(layerId) {
|
|
8489
8543
|
const layer = this.getLayer(layerId);
|
|
8490
|
-
if (!layer?.mask || layer.mask.source) return
|
|
8544
|
+
if (!layer?.mask || layer.mask.source) return null;
|
|
8491
8545
|
const { width, height } = this.naturalSize;
|
|
8492
8546
|
const ox = layer.type === "pixel" ? layer.offset.x : 0;
|
|
8493
8547
|
const oy = layer.type === "pixel" ? layer.offset.y : 0;
|
|
8494
8548
|
const data = readRegion(layer.mask.surface, { x: -ox, y: -oy, width, height });
|
|
8495
|
-
|
|
8496
|
-
return this.setSelection(invert2 ? invertSelection(sel) : sel, mode);
|
|
8549
|
+
return createSelectionMask(width, height, new Uint8Array(data.buffer, data.byteOffset, data.length));
|
|
8497
8550
|
}
|
|
8498
8551
|
// --- Histogram ---
|
|
8499
8552
|
/**
|
|
@@ -10111,6 +10164,10 @@ function isOpaqueInside(surface, width, height) {
|
|
|
10111
10164
|
}
|
|
10112
10165
|
return true;
|
|
10113
10166
|
}
|
|
10167
|
+
function collectSelectionTiles(selection, channels, into) {
|
|
10168
|
+
if (selection) collectTiles(selection.surface, into);
|
|
10169
|
+
for (const c of channels ?? []) collectTiles(c.mask.surface, into);
|
|
10170
|
+
}
|
|
10114
10171
|
function collectStateTiles(layers, into) {
|
|
10115
10172
|
for (const l of flattenLayers(layers)) {
|
|
10116
10173
|
if (l.type === "pixel" && l.source !== "document") collectTiles(l.surface, into);
|
|
@@ -10851,7 +10908,7 @@ var DEPTH_MASK_CONTROLS = [
|
|
|
10851
10908
|
// src/image-editor/playback.ts
|
|
10852
10909
|
var IMAGE_PLAYBACK_KIND = "layer";
|
|
10853
10910
|
var PAINT = /^(paint_stroke|clone_stamp|heal_brush|spot_heal|dodge|burn|sponge|smudge|blur_stroke|sharpen_stroke|history_brush|set_clone_source|set_clone_transform|fill_pixels|clear_pixels|set_paint|set_retouch_options|load_mixer_brush|clean_mixer_brush)$/;
|
|
10854
|
-
var SELECT = /^(select_region|select_subject|select_by_color|select_all|deselect|invert_selection|select_rect|select_ellipse|select_polygon|select_line|magic_wand|grow_selection|select_similar|reselect|select_layer_transparency|select_color_range|load_selection_from_image|load_selection_from_mask|set_selection_display|modify_selection|refine_selection|transform_selection|apply_selection_as_mask|layer_via_copy|stroke_selection|content_aware_fill|copy_pixels|cut_pixels|paste_pixels|save_selection|load_selection_from_channel|set_channel_properties|duplicate_channel|delete_channel|set_quick_mask)$/;
|
|
10911
|
+
var SELECT = /^(select_region|select_subject|select_by_color|select_all|deselect|invert_selection|select_rect|select_ellipse|select_polygon|select_line|magic_wand|grow_selection|select_similar|reselect|select_layer_transparency|select_color_range|load_selection_from_image|load_selection_from_mask|set_selection_display|modify_selection|refine_selection|transform_selection|apply_selection_as_mask|layer_via_copy|stroke_selection|content_aware_fill|copy_pixels|cut_pixels|paste_pixels|save_selection|load_selection_from_channel|set_channel_properties|duplicate_channel|delete_channel|load_channel_from_image|set_quick_mask)$/;
|
|
10855
10912
|
var ANNOTATE = /^(add_shape_layer|update_shape|add_shape_component|set_shape_component|remove_shape_component|merge_shape_components|add_text_layer|set_text|set_text_style|set_paragraph_style|set_text_frame|convert_text_to_shape)$/;
|
|
10856
10913
|
var CROP = /^(set_crop|crop_to_selection|reset_crop|set_rotation|flip_image|transform_layer|warp_layer|flip_layer|offset_layer)$/;
|
|
10857
10914
|
var ADJUST = /^(set_adjustments|add_adjustment_layer|update_adjustment_layer|remove_adjustment_layer|move_adjustment_layer)$/;
|
|
@@ -11020,9 +11077,7 @@ var REFINE_EDGE_CONTROLS = [
|
|
|
11020
11077
|
];
|
|
11021
11078
|
function describeSelection(mask) {
|
|
11022
11079
|
if (!mask || !mask.bounds) return "No selection";
|
|
11023
|
-
|
|
11024
|
-
for (let i = 0; i < mask.data.length; i++) sum += mask.data[i];
|
|
11025
|
-
const pct2 = sum / 255 / mask.data.length * 100;
|
|
11080
|
+
const pct2 = selectionArea(mask) / (mask.width * mask.height) * 100;
|
|
11026
11081
|
const shown = pct2 >= 10 ? Math.round(pct2) : Math.round(pct2 * 10) / 10;
|
|
11027
11082
|
return `${mask.bounds.width} \xD7 ${mask.bounds.height} px selected (${shown}% of the image)`;
|
|
11028
11083
|
}
|
|
@@ -11867,6 +11922,64 @@ function createImageSelectionTools(controller) {
|
|
|
11867
11922
|
return { quickMask: controller.quickMaskEditing, selection: summary(controller) };
|
|
11868
11923
|
}
|
|
11869
11924
|
};
|
|
11925
|
+
const exportMaskTool = {
|
|
11926
|
+
name: "export_mask_image",
|
|
11927
|
+
title: "Export a mask as a PNG",
|
|
11928
|
+
description: "Writes the selection, an alpha channel (channelId) or a layer's painted mask (layerId) as a greyscale PNG: white = selected / revealed. Use it to look at a mask on its own, or to hand it to another tool or service; load_channel_from_image and load_selection_from_image read it back. Give at most one of channelId and layerId (neither = the selection). Changes nothing. Returns the image and its size.",
|
|
11929
|
+
inputSchema: {
|
|
11930
|
+
type: "object",
|
|
11931
|
+
properties: {
|
|
11932
|
+
channelId: channelIdSchema,
|
|
11933
|
+
layerId: { type: "string", minLength: 1, description: "A layer whose painted mask to export (see list_layers)." },
|
|
11934
|
+
invert: { type: "boolean", description: "Black = selected instead (default false)." },
|
|
11935
|
+
maxSide: { type: "integer", minimum: 16, maximum: 16384, description: "Longest side in pixels; larger masks are scaled down (default 1024, never scaled up)." }
|
|
11936
|
+
},
|
|
11937
|
+
additionalProperties: false
|
|
11938
|
+
},
|
|
11939
|
+
annotations: { title: "Export a mask as a PNG", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
11940
|
+
handler: (args) => {
|
|
11941
|
+
if (args.channelId !== void 0 && args.layerId !== void 0) throw new Error("Give channelId or layerId, not both (neither exports the selection).");
|
|
11942
|
+
if (typeof args.layerId === "string" && !controller.getLayer(args.layerId)) throw new Error(`No layer "${args.layerId}". Call list_layers for ids.`);
|
|
11943
|
+
const source = typeof args.channelId === "string" ? { channelId: args.channelId } : typeof args.layerId === "string" ? { layerId: args.layerId } : "selection";
|
|
11944
|
+
const out = controller.exportMaskImage(source, { invert: args.invert === true, maxSide: typeof args.maxSide === "number" ? args.maxSide : 1024 });
|
|
11945
|
+
const what = source === "selection" ? "Selection" : "channelId" in source ? `Channel ${source.channelId}` : `Mask of layer ${source.layerId}`;
|
|
11946
|
+
return {
|
|
11947
|
+
content: [
|
|
11948
|
+
{ type: "image", data: out.dataUrl.slice(out.dataUrl.indexOf(",") + 1), mimeType: "image/png" },
|
|
11949
|
+
{ type: "text", text: `${what}, ${out.width}\xD7${out.height}` }
|
|
11950
|
+
],
|
|
11951
|
+
structuredContent: { width: out.width, height: out.height }
|
|
11952
|
+
};
|
|
11953
|
+
}
|
|
11954
|
+
};
|
|
11955
|
+
const loadChannelFromImageTool = {
|
|
11956
|
+
name: "load_channel_from_image",
|
|
11957
|
+
title: "Load a mask image into a new channel",
|
|
11958
|
+
description: "Stores a mask image (an AI segmentation, a matte, a PNG from export_mask_image) in a new alpha channel without touching the selection: white = selected by luminance, or one channel. Any size; it is resampled to the image. Load it as the selection later with load_selection_from_channel. One undo step. Returns the channel.",
|
|
11959
|
+
inputSchema: {
|
|
11960
|
+
type: "object",
|
|
11961
|
+
properties: {
|
|
11962
|
+
image: { type: "string", minLength: 1, description: "The mask as a data URL (data:image/png;base64,\u2026) or an image URL." },
|
|
11963
|
+
name: { type: "string", minLength: 1, description: 'Channel name (default "Alpha n").' },
|
|
11964
|
+
channel: { type: "string", enum: ["luminance", "alpha", "red", "green", "blue"], description: "What to read as coverage (default luminance; alpha for a cutout PNG)." },
|
|
11965
|
+
threshold: { type: "number", minimum: 0, maximum: 255, description: "Values at or above this are selected, the rest not (default: keep soft values)." },
|
|
11966
|
+
invert: { type: "boolean", description: "Black = selected instead (default false)." }
|
|
11967
|
+
},
|
|
11968
|
+
required: ["image"],
|
|
11969
|
+
additionalProperties: false
|
|
11970
|
+
},
|
|
11971
|
+
annotations: { ...writeAnnotations("Load a mask image into a new channel"), openWorldHint: true },
|
|
11972
|
+
handler: async (args) => {
|
|
11973
|
+
if (typeof args.image !== "string" || !args.image.trim()) throw new Error("image must be a data URL or image URL.");
|
|
11974
|
+
const c = await controller.loadChannelFromImage(args.image, {
|
|
11975
|
+
name: args.name,
|
|
11976
|
+
channel: args.channel ?? "luminance",
|
|
11977
|
+
invert: args.invert === true,
|
|
11978
|
+
threshold: typeof args.threshold === "number" ? Math.min(255, Math.max(0, args.threshold)) : void 0
|
|
11979
|
+
});
|
|
11980
|
+
return { channel: channelResult(c.id) };
|
|
11981
|
+
}
|
|
11982
|
+
};
|
|
11870
11983
|
return [
|
|
11871
11984
|
getSelection,
|
|
11872
11985
|
selectAll,
|
|
@@ -11902,6 +12015,8 @@ function createImageSelectionTools(controller) {
|
|
|
11902
12015
|
channelPropsTool,
|
|
11903
12016
|
duplicateChannelTool,
|
|
11904
12017
|
deleteChannelTool,
|
|
12018
|
+
exportMaskTool,
|
|
12019
|
+
loadChannelFromImageTool,
|
|
11905
12020
|
quickMaskTool
|
|
11906
12021
|
];
|
|
11907
12022
|
}
|
|
@@ -15963,6 +16078,39 @@ function handleQuickMaskKey(controller, e) {
|
|
|
15963
16078
|
return true;
|
|
15964
16079
|
}
|
|
15965
16080
|
|
|
16081
|
+
// src/image-editor/selection/mask-files.ts
|
|
16082
|
+
var MASK_FILE_ACCEPT = "image/png,image/jpeg,image/webp,image/gif,image/bmp";
|
|
16083
|
+
function blobDataUrl(file) {
|
|
16084
|
+
return new Promise((resolve, reject) => {
|
|
16085
|
+
const reader = new FileReader();
|
|
16086
|
+
reader.onload = () => resolve(String(reader.result));
|
|
16087
|
+
reader.onerror = () => reject(reader.error ?? new Error("The file could not be read."));
|
|
16088
|
+
reader.readAsDataURL(file);
|
|
16089
|
+
});
|
|
16090
|
+
}
|
|
16091
|
+
function maskNameFromFile(file) {
|
|
16092
|
+
const name = file.name;
|
|
16093
|
+
return name ? name.replace(/\.[a-z0-9]{2,4}$/i, "").trim() || void 0 : void 0;
|
|
16094
|
+
}
|
|
16095
|
+
async function openMaskFile(controller, file) {
|
|
16096
|
+
return controller.loadChannelFromImage(await blobDataUrl(file), { name: maskNameFromFile(file) });
|
|
16097
|
+
}
|
|
16098
|
+
function maskFileName(name) {
|
|
16099
|
+
const base = name.replace(/\.[a-z0-9]{2,4}$/i, "").replace(/[\\/:*?"<>|]+/g, "-").trim() || "mask";
|
|
16100
|
+
return `${base}.png`;
|
|
16101
|
+
}
|
|
16102
|
+
function downloadMaskImage(controller, source, name = "mask") {
|
|
16103
|
+
const { dataUrl, width, height } = controller.exportMaskImage(source);
|
|
16104
|
+
const fileName = maskFileName(name);
|
|
16105
|
+
if (typeof document !== "undefined") {
|
|
16106
|
+
const a = document.createElement("a");
|
|
16107
|
+
a.href = dataUrl;
|
|
16108
|
+
a.download = fileName;
|
|
16109
|
+
a.click();
|
|
16110
|
+
}
|
|
16111
|
+
return { fileName, width, height };
|
|
16112
|
+
}
|
|
16113
|
+
|
|
15966
16114
|
// src/image-editor/mosaic-filter.ts
|
|
15967
16115
|
function resolveMosaicRegionBounds(region, imgWidth, imgHeight) {
|
|
15968
16116
|
if (imgWidth <= 0 || imgHeight <= 0 || region.width <= 0 || region.height <= 0) {
|
|
@@ -17624,6 +17772,7 @@ export {
|
|
|
17624
17772
|
LayerEffectError,
|
|
17625
17773
|
LayerOperationError,
|
|
17626
17774
|
MARCHING_ANTS_STEP_MS,
|
|
17775
|
+
MASK_FILE_ACCEPT,
|
|
17627
17776
|
MAX_BATCH,
|
|
17628
17777
|
MAX_BRUSH_SIZE,
|
|
17629
17778
|
MAX_CANVAS_BACKING_SIDE,
|
|
@@ -17975,6 +18124,7 @@ export {
|
|
|
17975
18124
|
documentProfile,
|
|
17976
18125
|
dodgeBurn,
|
|
17977
18126
|
dodgeBurnCurve,
|
|
18127
|
+
downloadMaskImage,
|
|
17978
18128
|
downloadPsd,
|
|
17979
18129
|
dpow,
|
|
17980
18130
|
dragWarpMeshPoint,
|
|
@@ -18167,6 +18317,8 @@ export {
|
|
|
18167
18317
|
mapLayer,
|
|
18168
18318
|
mapSubpaths,
|
|
18169
18319
|
maskBounds,
|
|
18320
|
+
maskFileName,
|
|
18321
|
+
maskNameFromFile,
|
|
18170
18322
|
maskSummary,
|
|
18171
18323
|
maskThumbnailClick,
|
|
18172
18324
|
matrixFromCorners,
|
|
@@ -18224,6 +18376,7 @@ export {
|
|
|
18224
18376
|
offsetLayer,
|
|
18225
18377
|
offsetShape,
|
|
18226
18378
|
offsetSmartObject,
|
|
18379
|
+
openMaskFile,
|
|
18227
18380
|
openPsdFile,
|
|
18228
18381
|
packRgba,
|
|
18229
18382
|
packWetParams,
|
|
@@ -18269,6 +18422,7 @@ export {
|
|
|
18269
18422
|
pointerStrokeSample,
|
|
18270
18423
|
pointerStrokeSamples,
|
|
18271
18424
|
polygonCoverage,
|
|
18425
|
+
polygonCoverageBox,
|
|
18272
18426
|
polygonSelection,
|
|
18273
18427
|
posterizeValue,
|
|
18274
18428
|
presetWarp,
|
|
@@ -18368,6 +18522,8 @@ export {
|
|
|
18368
18522
|
selectionArea,
|
|
18369
18523
|
selectionFromCoverage,
|
|
18370
18524
|
selectionFromPixels,
|
|
18525
|
+
selectionFromRegion,
|
|
18526
|
+
selectionFromTiles,
|
|
18371
18527
|
selectionModeFromModifiers,
|
|
18372
18528
|
selectionOutline,
|
|
18373
18529
|
selectionRing,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Framework-free helpers behind the Import and Export PNG buttons of the React and Svelte Channels panels. */
|
|
2
|
+
import type { ImageEditorController, MaskImageSource } from '../controller.cjs';
|
|
3
|
+
import type { AlphaChannel } from './channels.cjs';
|
|
4
|
+
/** Types the mask file pickers accept. */
|
|
5
|
+
export declare const MASK_FILE_ACCEPT = "image/png,image/jpeg,image/webp,image/gif,image/bmp";
|
|
6
|
+
/** A channel name from a file name: without its extension, or `Alpha n`. */
|
|
7
|
+
export declare function maskNameFromFile(file: Blob): string | undefined;
|
|
8
|
+
/** Reads a picked mask image (white = selected) into a new alpha channel named after the file (one undo step). */
|
|
9
|
+
export declare function openMaskFile(controller: ImageEditorController, file: Blob): Promise<AlphaChannel>;
|
|
10
|
+
/** A file name for an exported mask: `name` with a `.png` extension. */
|
|
11
|
+
export declare function maskFileName(name: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* Writes the selection, a channel or a layer mask as a full-size greyscale PNG and offers it as a
|
|
14
|
+
* download (in a browser). Returns the file name and size.
|
|
15
|
+
*/
|
|
16
|
+
export declare function downloadMaskImage(controller: ImageEditorController, source: MaskImageSource, name?: string): {
|
|
17
|
+
fileName: string;
|
|
18
|
+
width: number;
|
|
19
|
+
height: number;
|
|
20
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Framework-free helpers behind the Import and Export PNG buttons of the React and Svelte Channels panels. */
|
|
2
|
+
import type { ImageEditorController, MaskImageSource } from '../controller.js';
|
|
3
|
+
import type { AlphaChannel } from './channels.js';
|
|
4
|
+
/** Types the mask file pickers accept. */
|
|
5
|
+
export declare const MASK_FILE_ACCEPT = "image/png,image/jpeg,image/webp,image/gif,image/bmp";
|
|
6
|
+
/** A channel name from a file name: without its extension, or `Alpha n`. */
|
|
7
|
+
export declare function maskNameFromFile(file: Blob): string | undefined;
|
|
8
|
+
/** Reads a picked mask image (white = selected) into a new alpha channel named after the file (one undo step). */
|
|
9
|
+
export declare function openMaskFile(controller: ImageEditorController, file: Blob): Promise<AlphaChannel>;
|
|
10
|
+
/** A file name for an exported mask: `name` with a `.png` extension. */
|
|
11
|
+
export declare function maskFileName(name: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* Writes the selection, a channel or a layer mask as a full-size greyscale PNG and offers it as a
|
|
14
|
+
* download (in a browser). Returns the file name and size.
|
|
15
|
+
*/
|
|
16
|
+
export declare function downloadMaskImage(controller: ImageEditorController, source: MaskImageSource, name?: string): {
|
|
17
|
+
fileName: string;
|
|
18
|
+
width: number;
|
|
19
|
+
height: number;
|
|
20
|
+
};
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import type { CropBox } from '../types.cjs';
|
|
2
|
+
import { TiledSurface } from '../../raster/surface.cjs';
|
|
2
3
|
/**
|
|
3
4
|
* A selection: one 8-bit coverage value per image pixel (0 = not selected, 255 = fully
|
|
4
5
|
* selected), row-major over the whole source image in natural pixels — the same space as the
|
|
5
6
|
* crop box and annotations. Soft values are partial selection (feathered or anti-aliased edges).
|
|
6
7
|
*
|
|
8
|
+
* The coverage is stored as 256 × 256 tiles (`surface`, one byte per pixel, missing tiles are 0
|
|
9
|
+
* and every pixel outside the image is 0), so an empty or fully selected area costs nothing and
|
|
10
|
+
* masks share the tiles an operation did not change. `data` is the same coverage as one array,
|
|
11
|
+
* built on first read and cached for the few masks read most recently.
|
|
12
|
+
*
|
|
7
13
|
* Treat a mask as immutable: every operation returns a new one, so history entries can share
|
|
8
|
-
* them without copying.
|
|
14
|
+
* them without copying. Never write to `data`.
|
|
9
15
|
*/
|
|
10
16
|
export interface SelectionMask {
|
|
11
17
|
readonly width: number;
|
|
@@ -13,6 +19,8 @@ export interface SelectionMask {
|
|
|
13
19
|
readonly data: Uint8Array;
|
|
14
20
|
/** Bounding box of the non-zero coverage, or `null` when nothing is selected. */
|
|
15
21
|
readonly bounds: CropBox | null;
|
|
22
|
+
/** The coverage as tiles (1 channel, default 0), shared with other masks where equal. */
|
|
23
|
+
readonly surface: TiledSurface;
|
|
16
24
|
}
|
|
17
25
|
/** How a new selection combines with the current one. */
|
|
18
26
|
export type SelectionMode = 'replace' | 'add' | 'subtract' | 'intersect';
|
|
@@ -35,8 +43,19 @@ export interface SerializedSelection {
|
|
|
35
43
|
}
|
|
36
44
|
/** Bounding box of non-zero bytes. */
|
|
37
45
|
export declare function computeSelectionBounds(width: number, height: number, data: ArrayLike<number>): CropBox | null;
|
|
46
|
+
/**
|
|
47
|
+
* A selection over `width × height` from a 1-channel tiled surface in image space (Quick Mask
|
|
48
|
+
* paint, a mask): its tiles are shared, and coverage outside the image is dropped. A surface
|
|
49
|
+
* whose default is not 0 is read out instead.
|
|
50
|
+
*/
|
|
51
|
+
export declare function selectionFromTiles(width: number, height: number, surface: TiledSurface): SelectionMask;
|
|
38
52
|
/** Wraps coverage bytes (taken over, not copied) as a selection, computing its bounds. */
|
|
39
53
|
export declare function createSelectionMask(width: number, height: number, data?: Uint8Array): SelectionMask;
|
|
54
|
+
/**
|
|
55
|
+
* A selection that is `data` (`rect.width × rect.height` coverage bytes) inside `rect` and
|
|
56
|
+
* nothing elsewhere, without allocating the whole image. `rect` must lie inside the image.
|
|
57
|
+
*/
|
|
58
|
+
export declare function selectionFromRegion(width: number, height: number, rect: CropBox, data: ArrayLike<number>): SelectionMask;
|
|
40
59
|
/** Select › All. */
|
|
41
60
|
export declare function fullSelection(width: number, height: number): SelectionMask;
|
|
42
61
|
export declare function isSelectionEmpty(mask: SelectionMask | null | undefined): boolean;
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import type { CropBox } from '../types.js';
|
|
2
|
+
import { TiledSurface } from '../../raster/surface.js';
|
|
2
3
|
/**
|
|
3
4
|
* A selection: one 8-bit coverage value per image pixel (0 = not selected, 255 = fully
|
|
4
5
|
* selected), row-major over the whole source image in natural pixels — the same space as the
|
|
5
6
|
* crop box and annotations. Soft values are partial selection (feathered or anti-aliased edges).
|
|
6
7
|
*
|
|
8
|
+
* The coverage is stored as 256 × 256 tiles (`surface`, one byte per pixel, missing tiles are 0
|
|
9
|
+
* and every pixel outside the image is 0), so an empty or fully selected area costs nothing and
|
|
10
|
+
* masks share the tiles an operation did not change. `data` is the same coverage as one array,
|
|
11
|
+
* built on first read and cached for the few masks read most recently.
|
|
12
|
+
*
|
|
7
13
|
* Treat a mask as immutable: every operation returns a new one, so history entries can share
|
|
8
|
-
* them without copying.
|
|
14
|
+
* them without copying. Never write to `data`.
|
|
9
15
|
*/
|
|
10
16
|
export interface SelectionMask {
|
|
11
17
|
readonly width: number;
|
|
@@ -13,6 +19,8 @@ export interface SelectionMask {
|
|
|
13
19
|
readonly data: Uint8Array;
|
|
14
20
|
/** Bounding box of the non-zero coverage, or `null` when nothing is selected. */
|
|
15
21
|
readonly bounds: CropBox | null;
|
|
22
|
+
/** The coverage as tiles (1 channel, default 0), shared with other masks where equal. */
|
|
23
|
+
readonly surface: TiledSurface;
|
|
16
24
|
}
|
|
17
25
|
/** How a new selection combines with the current one. */
|
|
18
26
|
export type SelectionMode = 'replace' | 'add' | 'subtract' | 'intersect';
|
|
@@ -35,8 +43,19 @@ export interface SerializedSelection {
|
|
|
35
43
|
}
|
|
36
44
|
/** Bounding box of non-zero bytes. */
|
|
37
45
|
export declare function computeSelectionBounds(width: number, height: number, data: ArrayLike<number>): CropBox | null;
|
|
46
|
+
/**
|
|
47
|
+
* A selection over `width × height` from a 1-channel tiled surface in image space (Quick Mask
|
|
48
|
+
* paint, a mask): its tiles are shared, and coverage outside the image is dropped. A surface
|
|
49
|
+
* whose default is not 0 is read out instead.
|
|
50
|
+
*/
|
|
51
|
+
export declare function selectionFromTiles(width: number, height: number, surface: TiledSurface): SelectionMask;
|
|
38
52
|
/** Wraps coverage bytes (taken over, not copied) as a selection, computing its bounds. */
|
|
39
53
|
export declare function createSelectionMask(width: number, height: number, data?: Uint8Array): SelectionMask;
|
|
54
|
+
/**
|
|
55
|
+
* A selection that is `data` (`rect.width × rect.height` coverage bytes) inside `rect` and
|
|
56
|
+
* nothing elsewhere, without allocating the whole image. `rect` must lie inside the image.
|
|
57
|
+
*/
|
|
58
|
+
export declare function selectionFromRegion(width: number, height: number, rect: CropBox, data: ArrayLike<number>): SelectionMask;
|
|
40
59
|
/** Select › All. */
|
|
41
60
|
export declare function fullSelection(width: number, height: number): SelectionMask;
|
|
42
61
|
export declare function isSelectionEmpty(mask: SelectionMask | null | undefined): boolean;
|
|
@@ -11,6 +11,14 @@ export interface ShapeSelectionOptions {
|
|
|
11
11
|
* their top), so the cost is about pixels + edges × rows they span.
|
|
12
12
|
*/
|
|
13
13
|
export declare function polygonCoverage(points: readonly ImagePoint[], width: number, height: number, antiAlias?: boolean): Float32Array;
|
|
14
|
+
/**
|
|
15
|
+
* {@link polygonCoverage} over just the polygon's bounding box (clipped to the image): `coverage`
|
|
16
|
+
* is `box.width × box.height`; `box` is `null` when the polygon covers no pixel row or column.
|
|
17
|
+
*/
|
|
18
|
+
export declare function polygonCoverageBox(points: readonly ImagePoint[], width: number, height: number, antiAlias?: boolean): {
|
|
19
|
+
box: CropBox | null;
|
|
20
|
+
coverage: Float32Array;
|
|
21
|
+
};
|
|
14
22
|
/** Lasso / polygonal lasso: an even-odd polygon in natural image pixels. */
|
|
15
23
|
export declare function polygonSelection(width: number, height: number, points: readonly ImagePoint[], options?: ShapeSelectionOptions): SelectionMask;
|
|
16
24
|
/** Normalises a rectangle with a negative width or height. */
|
|
@@ -11,6 +11,14 @@ export interface ShapeSelectionOptions {
|
|
|
11
11
|
* their top), so the cost is about pixels + edges × rows they span.
|
|
12
12
|
*/
|
|
13
13
|
export declare function polygonCoverage(points: readonly ImagePoint[], width: number, height: number, antiAlias?: boolean): Float32Array;
|
|
14
|
+
/**
|
|
15
|
+
* {@link polygonCoverage} over just the polygon's bounding box (clipped to the image): `coverage`
|
|
16
|
+
* is `box.width × box.height`; `box` is `null` when the polygon covers no pixel row or column.
|
|
17
|
+
*/
|
|
18
|
+
export declare function polygonCoverageBox(points: readonly ImagePoint[], width: number, height: number, antiAlias?: boolean): {
|
|
19
|
+
box: CropBox | null;
|
|
20
|
+
coverage: Float32Array;
|
|
21
|
+
};
|
|
14
22
|
/** Lasso / polygonal lasso: an even-odd polygon in natural image pixels. */
|
|
15
23
|
export declare function polygonSelection(width: number, height: number, points: readonly ImagePoint[], options?: ShapeSelectionOptions): SelectionMask;
|
|
16
24
|
/** Normalises a rectangle with a negative width or height. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type SelectionMask, type SelectionMode } from './mask.cjs';
|
|
2
2
|
import type { ColorDistance, ColorRangePreset } from './wand.cjs';
|
|
3
3
|
import type { RefineEdgeParams } from './refine.cjs';
|
|
4
4
|
/** Selection tools of the image editor UI. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type SelectionMask, type SelectionMode } from './mask.js';
|
|
2
2
|
import type { ColorDistance, ColorRangePreset } from './wand.js';
|
|
3
3
|
import type { RefineEdgeParams } from './refine.js';
|
|
4
4
|
/** Selection tools of the image editor UI. */
|