@pixerate/editor 0.88.0 → 0.89.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.
Files changed (65) hide show
  1. package/THIRD_PARTY_NOTICES.md +8 -2
  2. package/dist/agent/index.cjs +28 -28
  3. package/dist/agent/index.js +3 -3
  4. package/dist/canvas/index.cjs +3 -3
  5. package/dist/canvas/index.cjs.map +1 -1
  6. package/dist/canvas/index.js +16 -16
  7. package/dist/{chunk-TYOAJDFD.cjs → chunk-66IMF6OD.cjs} +3 -3
  8. package/dist/{chunk-TYOAJDFD.cjs.map → chunk-66IMF6OD.cjs.map} +1 -1
  9. package/dist/chunk-D4ZCU72J.js +604 -0
  10. package/dist/chunk-D4ZCU72J.js.map +1 -0
  11. package/dist/{chunk-IMSX4IVG.js → chunk-FWEO3HSL.js} +4 -4
  12. package/dist/chunk-H543GD6T.cjs +1696 -0
  13. package/dist/chunk-H543GD6T.cjs.map +1 -0
  14. package/dist/{chunk-ROXFP2LQ.cjs → chunk-HNDSPZ5T.cjs} +4 -4
  15. package/dist/{chunk-ROXFP2LQ.cjs.map → chunk-HNDSPZ5T.cjs.map} +1 -1
  16. package/dist/{chunk-MXF4DP2C.cjs → chunk-J3B45HTA.cjs} +74 -74
  17. package/dist/{chunk-MXF4DP2C.cjs.map → chunk-J3B45HTA.cjs.map} +1 -1
  18. package/dist/{chunk-GNO5PEMH.js → chunk-OK7T2LHK.js} +2 -2
  19. package/dist/chunk-P5TPUNBZ.js +1696 -0
  20. package/dist/chunk-P5TPUNBZ.js.map +1 -0
  21. package/dist/chunk-PDCITYBT.cjs +416 -0
  22. package/dist/chunk-PDCITYBT.cjs.map +1 -0
  23. package/dist/{chunk-RKJDBXTY.cjs → chunk-T4XWVPJK.cjs} +3 -3
  24. package/dist/{chunk-RKJDBXTY.cjs.map → chunk-T4XWVPJK.cjs.map} +1 -1
  25. package/dist/{chunk-ZQNDSUZO.js → chunk-UKI64KUD.js} +6 -6
  26. package/dist/{chunk-SVW2UNUC.js → chunk-WON34HKM.js} +2 -2
  27. package/dist/chunk-YLB2WVNA.js +416 -0
  28. package/dist/chunk-YLB2WVNA.js.map +1 -0
  29. package/dist/chunk-ZOOEX3WT.cjs +604 -0
  30. package/dist/chunk-ZOOEX3WT.cjs.map +1 -0
  31. package/dist/extensions/index.cjs +6 -6
  32. package/dist/extensions/index.cjs.map +1 -1
  33. package/dist/extensions/index.js +5 -5
  34. package/dist/image-editor/index.cjs +4387 -690
  35. package/dist/image-editor/index.cjs.map +1 -1
  36. package/dist/image-editor/index.d.cts +1677 -590
  37. package/dist/image-editor/index.d.ts +1677 -590
  38. package/dist/image-editor/index.js +4273 -576
  39. package/dist/image-editor/index.js.map +1 -1
  40. package/dist/index.cjs +27 -27
  41. package/dist/index.cjs.map +1 -1
  42. package/dist/index.js +10 -10
  43. package/dist/paint/index.cjs +38 -0
  44. package/dist/paint/index.cjs.map +1 -0
  45. package/dist/paint/index.d.cts +167 -0
  46. package/dist/paint/index.d.ts +167 -0
  47. package/dist/paint/index.js +38 -0
  48. package/dist/paint/index.js.map +1 -0
  49. package/dist/raster/index.cjs +121 -0
  50. package/dist/raster/index.cjs.map +1 -0
  51. package/dist/raster/index.d.cts +365 -0
  52. package/dist/raster/index.d.ts +365 -0
  53. package/dist/raster/index.js +121 -0
  54. package/dist/raster/index.js.map +1 -0
  55. package/dist/serializers/index.cjs +4 -4
  56. package/dist/serializers/index.cjs.map +1 -1
  57. package/dist/serializers/index.js +3 -3
  58. package/dist/surface-fJhkmNQn.d.cts +181 -0
  59. package/dist/surface-fJhkmNQn.d.ts +181 -0
  60. package/package.json +13 -2
  61. package/skills/pixerate-image-editor/SKILL.md +111 -11
  62. /package/dist/{chunk-IMSX4IVG.js.map → chunk-FWEO3HSL.js.map} +0 -0
  63. /package/dist/{chunk-GNO5PEMH.js.map → chunk-OK7T2LHK.js.map} +0 -0
  64. /package/dist/{chunk-ZQNDSUZO.js.map → chunk-UKI64KUD.js.map} +0 -0
  65. /package/dist/{chunk-SVW2UNUC.js.map → chunk-WON34HKM.js.map} +0 -0
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Photoshop blend modes on straight (non-premultiplied) 0..1 colours.
3
+ *
4
+ * Formulas follow the W3C Compositing / ISO 32000 definitions, with Photoshop's variants where
5
+ * they differ (Soft Light, Vivid / Linear / Pin Light, Hard Mix, Linear Burn / Dodge, Subtract,
6
+ * Divide, Darker / Lighter Color), and the hardened `ClipColor` for the non-separable modes.
7
+ */
8
+ type BlendMode = 'normal' | 'darken' | 'multiply' | 'color-burn' | 'linear-burn' | 'darker-color' | 'lighten' | 'screen' | 'color-dodge' | 'linear-dodge' | 'lighter-color' | 'overlay' | 'soft-light' | 'hard-light' | 'vivid-light' | 'linear-light' | 'pin-light' | 'hard-mix' | 'difference' | 'exclusion' | 'subtract' | 'divide' | 'hue' | 'saturation' | 'color' | 'luminosity';
9
+ interface BlendModeInfo {
10
+ id: BlendMode;
11
+ label: string;
12
+ /** Canvas `globalCompositeOperation` with the same maths, when there is one. */
13
+ canvas: GlobalCompositeOperation | null;
14
+ }
15
+ /** Every blend mode, in Photoshop's menu order. */
16
+ declare const BLEND_MODES: readonly BlendModeInfo[];
17
+ declare function isBlendMode(value: unknown): value is BlendMode;
18
+ /** A stored blend mode, or `normal` for a missing / unknown value. */
19
+ declare function normalizeBlendMode(value: unknown): BlendMode;
20
+ /**
21
+ * The canvas composite operation that implements `mode` exactly, or `null` when the canvas has
22
+ * none (draw through {@link blendImageData} instead).
23
+ */
24
+ declare function canvasCompositeOperation(mode: BlendMode | undefined): GlobalCompositeOperation | null;
25
+ /** Whether a mode works on whole colours (hue, saturation, colour, luminosity, darker / lighter colour). */
26
+ declare function isSeparableBlendMode(mode: BlendMode): boolean;
27
+ /** Separable blend function `B(cb, cs)` of one channel. Non-separable modes return `cs`. */
28
+ declare function blendChannel(mode: BlendMode, cb: number, cs: number): number;
29
+ type Rgb = [number, number, number];
30
+ type Rgba = [number, number, number, number];
31
+ /** Luminosity of the non-separable modes (W3C / ISO 32000 weights). */
32
+ declare function blendLum(c: readonly number[]): number;
33
+ /** `c` with its luminosity moved to `l`, clipped back into gamut towards grey (SetLum + ClipColor). */
34
+ declare function setLum(c: readonly number[], l: number): Rgb;
35
+ /** `B(Cb, Cs)` of a whole colour, including the non-separable modes. */
36
+ declare function blendRgb(mode: BlendMode, cb: readonly number[], cs: readonly number[]): Rgb;
37
+ /**
38
+ * Composites a straight-alpha `source` over a straight-alpha `backdrop` (both RGBA in 0..1) with a
39
+ * blend mode and an extra `alpha` (opacity), using the W3C general formula:
40
+ * `αo = αs + αb(1 − αs)`, `αo·Co = (1 − αs)·αb·Cb + (1 − αb)·αs·Cs + αs·αb·B(Cb, Cs)`.
41
+ */
42
+ declare function blendPixel(mode: BlendMode, backdrop: readonly number[], source: readonly number[], alpha?: number): Rgba;
43
+ /**
44
+ * Blends `source` into `backdrop` in place (8-bit straight RGBA of the same size) with
45
+ * {@link blendPixel}. The scalar fallback for modes the canvas cannot composite.
46
+ */
47
+ declare function blendImageData(backdrop: ImageData, source: ImageData, mode: BlendMode, alpha?: number): void;
48
+
49
+ /**
50
+ * Tiled, sparse, copy-on-write pixel storage (photocraft's `Surface`): 256 px square tiles in a
51
+ * map keyed by tile coordinate, a default value for every missing tile, and shared tiles.
52
+ *
53
+ * - Pixels are 8-bit, straight alpha: RGBA for layers (4 channels, the same as `ImageData`), one
54
+ * byte for masks.
55
+ * - A surface is never mutated. A write returns a new surface that shares every tile it did not
56
+ * touch, so undo history and caches can keep old surfaces by reference.
57
+ * - A tile whose pixels all hold one value is a *uniform* tile; one instance per value is shared,
58
+ * so a solid fill or a "hide all" mask costs one tile however large it is.
59
+ * - Coordinates are document pixels and may be negative or past the canvas (layers can extend
60
+ * beyond it, as PSD layer rects do).
61
+ *
62
+ * The module has no DOM dependency, so it runs in Node, tests and workers.
63
+ */
64
+ /** A pixel rectangle (`x`, `y` of the top-left corner, `width`, `height`). */
65
+ interface RasterRect {
66
+ x: number;
67
+ y: number;
68
+ width: number;
69
+ height: number;
70
+ }
71
+ type CropBox = RasterRect;
72
+ declare const TILE = 256;
73
+ declare const TILE_SHIFT = 8;
74
+ /** A tile coordinate packed into one number (tx, ty in −32768..32767). */
75
+ type TileKey = number;
76
+ declare function tileKey(tx: number, ty: number): TileKey;
77
+ declare function tileCoord(key: TileKey): {
78
+ tx: number;
79
+ ty: number;
80
+ };
81
+ /** A tile's pixel rectangle in document space. */
82
+ declare function tileRect(tx: number, ty: number): CropBox;
83
+ /** One tile: TILE × TILE pixels of `channels` bytes. Never mutate `data` after it is shared. */
84
+ interface Tile {
85
+ readonly data: Uint8ClampedArray;
86
+ /** The packed value of every pixel when the tile is uniform, else `null`. */
87
+ readonly uniform: number | null;
88
+ }
89
+ /**
90
+ * An immutable tiled surface. `channels` is 4 (RGBA) for layers and 1 for masks. Missing tiles
91
+ * hold `defaultValue` in every byte (0: transparent black for layers; 0 or 255 for masks).
92
+ */
93
+ declare class TiledSurface {
94
+ readonly channels: 1 | 4;
95
+ readonly tiles: ReadonlyMap<TileKey, Tile>;
96
+ readonly defaultValue: number;
97
+ /** Unique per instance: a cache key that changes whenever the pixels can have changed. */
98
+ readonly id: number;
99
+ private boundsCache;
100
+ constructor(channels: 1 | 4, tiles: ReadonlyMap<TileKey, Tile>, defaultValue?: number);
101
+ /** Bounds of the stored (non-default) tiles, tile-aligned; `null` when there are none. */
102
+ get bounds(): CropBox | null;
103
+ get tileCount(): number;
104
+ getTile(tx: number, ty: number): Tile | undefined;
105
+ }
106
+ type PixelSurface = TiledSurface;
107
+ type MaskSurface = TiledSurface;
108
+ /** The shared tile whose every pixel is `value` (packed RGBA, or one byte for masks). */
109
+ declare function uniformTile(channels: 1 | 4, value: number): Tile;
110
+ /** Packs an RGBA colour (0..255 each) the way uniform tiles store it. */
111
+ declare function packRgba(r: number, g: number, b: number, a: number): number;
112
+ /**
113
+ * Turns freshly written tile data into a stored tile: `null` when it equals the surface default
114
+ * (the tile is dropped), the shared uniform tile when it is uniform, else a tile owning `data`.
115
+ */
116
+ declare function finishTile(data: Uint8ClampedArray, channels: 1 | 4, defaultValue: number): Tile | null;
117
+ /** An empty surface (every pixel is `defaultValue`). */
118
+ declare function emptySurface(channels?: 1 | 4, defaultValue?: number): TiledSurface;
119
+ /** A surface filled with one value over `rect` (packed RGBA for layers, a byte for masks). */
120
+ declare function solidSurface(channels: 1 | 4, rect: CropBox, value: number, defaultValue?: number): TiledSurface;
121
+ /**
122
+ * A surface holding `width × height` interleaved pixels placed at (`x`, `y`) in document space.
123
+ * Tiles equal to the default are left out; uniform tiles are shared.
124
+ */
125
+ declare function surfaceFromPixels(pixels: {
126
+ width: number;
127
+ height: number;
128
+ data: ArrayLike<number>;
129
+ }, channels?: 1 | 4, x?: number, y?: number, defaultValue?: number): TiledSurface;
130
+ /** Tile coordinates covering `rect`, row by row. */
131
+ declare function tilesInRect(rect: CropBox): Array<{
132
+ tx: number;
133
+ ty: number;
134
+ }>;
135
+ /**
136
+ * Copies `rect` of the surface into `out` (or a new buffer), `rect.width × rect.height`
137
+ * interleaved pixels; missing tiles read as the default.
138
+ */
139
+ declare function readRegion(surface: TiledSurface, rect: CropBox, out?: Uint8ClampedArray): Uint8ClampedArray;
140
+ /** One pixel's bytes (RGBA or the mask byte). */
141
+ declare function readPixel(surface: TiledSurface, x: number, y: number): number[];
142
+ /**
143
+ * Exact bounds of the pixels that differ from the default (for a layer: non-transparent), or
144
+ * `null`. Scans the stored tiles; uniform tiles count whole.
145
+ */
146
+ declare function contentBounds(surface: TiledSurface): CropBox | null;
147
+ /**
148
+ * A private, mutable copy of the tiles a write touches. `tile(tx, ty)` copies the tile on first
149
+ * use; `commit()` returns a new surface sharing every untouched tile. Use it for strokes:
150
+ * edit through the draft while the pointer moves and commit once at pointer-up.
151
+ */
152
+ declare class SurfaceDraft {
153
+ readonly base: TiledSurface;
154
+ private readonly copies;
155
+ constructor(base: TiledSurface);
156
+ get channels(): 1 | 4;
157
+ /** The writable data of tile (tx, ty), copied from the base on first use. */
158
+ tile(tx: number, ty: number): Uint8ClampedArray;
159
+ /** Replaces a whole tile with `data` (owned by the draft from now on). */
160
+ replace(tx: number, ty: number, data: Uint8ClampedArray): void;
161
+ /** The current data of a tile without copying it (base tile, draft copy or `null` for default). */
162
+ peek(tx: number, ty: number): Uint8ClampedArray | null;
163
+ /** Keys of the tiles changed so far. */
164
+ get touched(): TileKey[];
165
+ /** A new surface with the draft's tiles; the draft must not be used afterwards. */
166
+ commit(): TiledSurface;
167
+ }
168
+ /** Writes `rect.width × rect.height` interleaved pixels at `rect`; returns the new surface. */
169
+ declare function writeRegion(surface: TiledSurface, rect: CropBox, data: ArrayLike<number>): TiledSurface;
170
+ /** Fills `rect` with one pixel value (`bytes`, `channels` long). */
171
+ declare function fillRect(surface: TiledSurface, rect: CropBox, bytes: ArrayLike<number>): TiledSurface;
172
+ /** A copy of the surface moved by an integer offset (re-tiles; layers normally move by `offset`). */
173
+ declare function translateSurface(surface: TiledSurface, dx: number, dy: number): TiledSurface;
174
+ /** Bytes held by a surface's non-uniform tiles (uniform tiles are shared and nearly free). */
175
+ declare function surfaceBytes(surface: TiledSurface): number;
176
+ /** Adds every non-uniform tile of `surface` to `into` (for counting bytes shared across states). */
177
+ declare function collectTiles(surface: TiledSurface, into: Set<Tile>): void;
178
+ /** True when two surfaces hold the same pixels (cheap when they share tiles). */
179
+ declare function surfacesEqual(a: TiledSurface, b: TiledSurface): boolean;
180
+
181
+ export { solidSurface as A, type BlendMode as B, surfaceBytes as C, surfaceFromPixels as D, surfacesEqual as E, tileCoord as F, tileKey as G, tileRect as H, tilesInRect as I, translateSurface as J, uniformTile as K, writeRegion as L, type MaskSurface as M, type PixelSurface as P, type RasterRect as R, SurfaceDraft as S, TiledSurface as T, BLEND_MODES as a, type BlendModeInfo as b, type Rgb as c, type Rgba as d, TILE as e, TILE_SHIFT as f, type Tile as g, type TileKey as h, blendChannel as i, blendImageData as j, blendLum as k, blendPixel as l, blendRgb as m, canvasCompositeOperation as n, collectTiles as o, contentBounds as p, emptySurface as q, fillRect as r, finishTile as s, isBlendMode as t, isSeparableBlendMode as u, normalizeBlendMode as v, packRgba as w, readPixel as x, readRegion as y, setLum as z };
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Photoshop blend modes on straight (non-premultiplied) 0..1 colours.
3
+ *
4
+ * Formulas follow the W3C Compositing / ISO 32000 definitions, with Photoshop's variants where
5
+ * they differ (Soft Light, Vivid / Linear / Pin Light, Hard Mix, Linear Burn / Dodge, Subtract,
6
+ * Divide, Darker / Lighter Color), and the hardened `ClipColor` for the non-separable modes.
7
+ */
8
+ type BlendMode = 'normal' | 'darken' | 'multiply' | 'color-burn' | 'linear-burn' | 'darker-color' | 'lighten' | 'screen' | 'color-dodge' | 'linear-dodge' | 'lighter-color' | 'overlay' | 'soft-light' | 'hard-light' | 'vivid-light' | 'linear-light' | 'pin-light' | 'hard-mix' | 'difference' | 'exclusion' | 'subtract' | 'divide' | 'hue' | 'saturation' | 'color' | 'luminosity';
9
+ interface BlendModeInfo {
10
+ id: BlendMode;
11
+ label: string;
12
+ /** Canvas `globalCompositeOperation` with the same maths, when there is one. */
13
+ canvas: GlobalCompositeOperation | null;
14
+ }
15
+ /** Every blend mode, in Photoshop's menu order. */
16
+ declare const BLEND_MODES: readonly BlendModeInfo[];
17
+ declare function isBlendMode(value: unknown): value is BlendMode;
18
+ /** A stored blend mode, or `normal` for a missing / unknown value. */
19
+ declare function normalizeBlendMode(value: unknown): BlendMode;
20
+ /**
21
+ * The canvas composite operation that implements `mode` exactly, or `null` when the canvas has
22
+ * none (draw through {@link blendImageData} instead).
23
+ */
24
+ declare function canvasCompositeOperation(mode: BlendMode | undefined): GlobalCompositeOperation | null;
25
+ /** Whether a mode works on whole colours (hue, saturation, colour, luminosity, darker / lighter colour). */
26
+ declare function isSeparableBlendMode(mode: BlendMode): boolean;
27
+ /** Separable blend function `B(cb, cs)` of one channel. Non-separable modes return `cs`. */
28
+ declare function blendChannel(mode: BlendMode, cb: number, cs: number): number;
29
+ type Rgb = [number, number, number];
30
+ type Rgba = [number, number, number, number];
31
+ /** Luminosity of the non-separable modes (W3C / ISO 32000 weights). */
32
+ declare function blendLum(c: readonly number[]): number;
33
+ /** `c` with its luminosity moved to `l`, clipped back into gamut towards grey (SetLum + ClipColor). */
34
+ declare function setLum(c: readonly number[], l: number): Rgb;
35
+ /** `B(Cb, Cs)` of a whole colour, including the non-separable modes. */
36
+ declare function blendRgb(mode: BlendMode, cb: readonly number[], cs: readonly number[]): Rgb;
37
+ /**
38
+ * Composites a straight-alpha `source` over a straight-alpha `backdrop` (both RGBA in 0..1) with a
39
+ * blend mode and an extra `alpha` (opacity), using the W3C general formula:
40
+ * `αo = αs + αb(1 − αs)`, `αo·Co = (1 − αs)·αb·Cb + (1 − αb)·αs·Cs + αs·αb·B(Cb, Cs)`.
41
+ */
42
+ declare function blendPixel(mode: BlendMode, backdrop: readonly number[], source: readonly number[], alpha?: number): Rgba;
43
+ /**
44
+ * Blends `source` into `backdrop` in place (8-bit straight RGBA of the same size) with
45
+ * {@link blendPixel}. The scalar fallback for modes the canvas cannot composite.
46
+ */
47
+ declare function blendImageData(backdrop: ImageData, source: ImageData, mode: BlendMode, alpha?: number): void;
48
+
49
+ /**
50
+ * Tiled, sparse, copy-on-write pixel storage (photocraft's `Surface`): 256 px square tiles in a
51
+ * map keyed by tile coordinate, a default value for every missing tile, and shared tiles.
52
+ *
53
+ * - Pixels are 8-bit, straight alpha: RGBA for layers (4 channels, the same as `ImageData`), one
54
+ * byte for masks.
55
+ * - A surface is never mutated. A write returns a new surface that shares every tile it did not
56
+ * touch, so undo history and caches can keep old surfaces by reference.
57
+ * - A tile whose pixels all hold one value is a *uniform* tile; one instance per value is shared,
58
+ * so a solid fill or a "hide all" mask costs one tile however large it is.
59
+ * - Coordinates are document pixels and may be negative or past the canvas (layers can extend
60
+ * beyond it, as PSD layer rects do).
61
+ *
62
+ * The module has no DOM dependency, so it runs in Node, tests and workers.
63
+ */
64
+ /** A pixel rectangle (`x`, `y` of the top-left corner, `width`, `height`). */
65
+ interface RasterRect {
66
+ x: number;
67
+ y: number;
68
+ width: number;
69
+ height: number;
70
+ }
71
+ type CropBox = RasterRect;
72
+ declare const TILE = 256;
73
+ declare const TILE_SHIFT = 8;
74
+ /** A tile coordinate packed into one number (tx, ty in −32768..32767). */
75
+ type TileKey = number;
76
+ declare function tileKey(tx: number, ty: number): TileKey;
77
+ declare function tileCoord(key: TileKey): {
78
+ tx: number;
79
+ ty: number;
80
+ };
81
+ /** A tile's pixel rectangle in document space. */
82
+ declare function tileRect(tx: number, ty: number): CropBox;
83
+ /** One tile: TILE × TILE pixels of `channels` bytes. Never mutate `data` after it is shared. */
84
+ interface Tile {
85
+ readonly data: Uint8ClampedArray;
86
+ /** The packed value of every pixel when the tile is uniform, else `null`. */
87
+ readonly uniform: number | null;
88
+ }
89
+ /**
90
+ * An immutable tiled surface. `channels` is 4 (RGBA) for layers and 1 for masks. Missing tiles
91
+ * hold `defaultValue` in every byte (0: transparent black for layers; 0 or 255 for masks).
92
+ */
93
+ declare class TiledSurface {
94
+ readonly channels: 1 | 4;
95
+ readonly tiles: ReadonlyMap<TileKey, Tile>;
96
+ readonly defaultValue: number;
97
+ /** Unique per instance: a cache key that changes whenever the pixels can have changed. */
98
+ readonly id: number;
99
+ private boundsCache;
100
+ constructor(channels: 1 | 4, tiles: ReadonlyMap<TileKey, Tile>, defaultValue?: number);
101
+ /** Bounds of the stored (non-default) tiles, tile-aligned; `null` when there are none. */
102
+ get bounds(): CropBox | null;
103
+ get tileCount(): number;
104
+ getTile(tx: number, ty: number): Tile | undefined;
105
+ }
106
+ type PixelSurface = TiledSurface;
107
+ type MaskSurface = TiledSurface;
108
+ /** The shared tile whose every pixel is `value` (packed RGBA, or one byte for masks). */
109
+ declare function uniformTile(channels: 1 | 4, value: number): Tile;
110
+ /** Packs an RGBA colour (0..255 each) the way uniform tiles store it. */
111
+ declare function packRgba(r: number, g: number, b: number, a: number): number;
112
+ /**
113
+ * Turns freshly written tile data into a stored tile: `null` when it equals the surface default
114
+ * (the tile is dropped), the shared uniform tile when it is uniform, else a tile owning `data`.
115
+ */
116
+ declare function finishTile(data: Uint8ClampedArray, channels: 1 | 4, defaultValue: number): Tile | null;
117
+ /** An empty surface (every pixel is `defaultValue`). */
118
+ declare function emptySurface(channels?: 1 | 4, defaultValue?: number): TiledSurface;
119
+ /** A surface filled with one value over `rect` (packed RGBA for layers, a byte for masks). */
120
+ declare function solidSurface(channels: 1 | 4, rect: CropBox, value: number, defaultValue?: number): TiledSurface;
121
+ /**
122
+ * A surface holding `width × height` interleaved pixels placed at (`x`, `y`) in document space.
123
+ * Tiles equal to the default are left out; uniform tiles are shared.
124
+ */
125
+ declare function surfaceFromPixels(pixels: {
126
+ width: number;
127
+ height: number;
128
+ data: ArrayLike<number>;
129
+ }, channels?: 1 | 4, x?: number, y?: number, defaultValue?: number): TiledSurface;
130
+ /** Tile coordinates covering `rect`, row by row. */
131
+ declare function tilesInRect(rect: CropBox): Array<{
132
+ tx: number;
133
+ ty: number;
134
+ }>;
135
+ /**
136
+ * Copies `rect` of the surface into `out` (or a new buffer), `rect.width × rect.height`
137
+ * interleaved pixels; missing tiles read as the default.
138
+ */
139
+ declare function readRegion(surface: TiledSurface, rect: CropBox, out?: Uint8ClampedArray): Uint8ClampedArray;
140
+ /** One pixel's bytes (RGBA or the mask byte). */
141
+ declare function readPixel(surface: TiledSurface, x: number, y: number): number[];
142
+ /**
143
+ * Exact bounds of the pixels that differ from the default (for a layer: non-transparent), or
144
+ * `null`. Scans the stored tiles; uniform tiles count whole.
145
+ */
146
+ declare function contentBounds(surface: TiledSurface): CropBox | null;
147
+ /**
148
+ * A private, mutable copy of the tiles a write touches. `tile(tx, ty)` copies the tile on first
149
+ * use; `commit()` returns a new surface sharing every untouched tile. Use it for strokes:
150
+ * edit through the draft while the pointer moves and commit once at pointer-up.
151
+ */
152
+ declare class SurfaceDraft {
153
+ readonly base: TiledSurface;
154
+ private readonly copies;
155
+ constructor(base: TiledSurface);
156
+ get channels(): 1 | 4;
157
+ /** The writable data of tile (tx, ty), copied from the base on first use. */
158
+ tile(tx: number, ty: number): Uint8ClampedArray;
159
+ /** Replaces a whole tile with `data` (owned by the draft from now on). */
160
+ replace(tx: number, ty: number, data: Uint8ClampedArray): void;
161
+ /** The current data of a tile without copying it (base tile, draft copy or `null` for default). */
162
+ peek(tx: number, ty: number): Uint8ClampedArray | null;
163
+ /** Keys of the tiles changed so far. */
164
+ get touched(): TileKey[];
165
+ /** A new surface with the draft's tiles; the draft must not be used afterwards. */
166
+ commit(): TiledSurface;
167
+ }
168
+ /** Writes `rect.width × rect.height` interleaved pixels at `rect`; returns the new surface. */
169
+ declare function writeRegion(surface: TiledSurface, rect: CropBox, data: ArrayLike<number>): TiledSurface;
170
+ /** Fills `rect` with one pixel value (`bytes`, `channels` long). */
171
+ declare function fillRect(surface: TiledSurface, rect: CropBox, bytes: ArrayLike<number>): TiledSurface;
172
+ /** A copy of the surface moved by an integer offset (re-tiles; layers normally move by `offset`). */
173
+ declare function translateSurface(surface: TiledSurface, dx: number, dy: number): TiledSurface;
174
+ /** Bytes held by a surface's non-uniform tiles (uniform tiles are shared and nearly free). */
175
+ declare function surfaceBytes(surface: TiledSurface): number;
176
+ /** Adds every non-uniform tile of `surface` to `into` (for counting bytes shared across states). */
177
+ declare function collectTiles(surface: TiledSurface, into: Set<Tile>): void;
178
+ /** True when two surfaces hold the same pixels (cheap when they share tiles). */
179
+ declare function surfacesEqual(a: TiledSurface, b: TiledSurface): boolean;
180
+
181
+ export { solidSurface as A, type BlendMode as B, surfaceBytes as C, surfaceFromPixels as D, surfacesEqual as E, tileCoord as F, tileKey as G, tileRect as H, tilesInRect as I, translateSurface as J, uniformTile as K, writeRegion as L, type MaskSurface as M, type PixelSurface as P, type RasterRect as R, SurfaceDraft as S, TiledSurface as T, BLEND_MODES as a, type BlendModeInfo as b, type Rgb as c, type Rgba as d, TILE as e, TILE_SHIFT as f, type Tile as g, type TileKey as h, blendChannel as i, blendImageData as j, blendLum as k, blendPixel as l, blendRgb as m, canvasCompositeOperation as n, collectTiles as o, contentBounds as p, emptySurface as q, fillRect as r, finishTile as s, isBlendMode as t, isSeparableBlendMode as u, normalizeBlendMode as v, packRgba as w, readPixel as x, readRegion as y, setLum as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixerate/editor",
3
- "version": "0.88.0",
3
+ "version": "0.89.0",
4
4
  "description": "UI-agnostic rich text and media editing core library",
5
5
  "author": "Pixerate",
6
6
  "license": "MIT",
@@ -74,6 +74,16 @@
74
74
  "import": "./dist/tools/index.js",
75
75
  "require": "./dist/tools/index.cjs"
76
76
  },
77
+ "./raster": {
78
+ "types": "./dist/raster/index.d.ts",
79
+ "import": "./dist/raster/index.js",
80
+ "require": "./dist/raster/index.cjs"
81
+ },
82
+ "./paint": {
83
+ "types": "./dist/paint/index.d.ts",
84
+ "import": "./dist/paint/index.js",
85
+ "require": "./dist/paint/index.cjs"
86
+ },
77
87
  "./image-editor": {
78
88
  "types": "./dist/image-editor/index.d.ts",
79
89
  "import": "./dist/image-editor/index.js",
@@ -197,6 +207,7 @@
197
207
  "scripts": {
198
208
  "build": "tsup",
199
209
  "typecheck": "tsc --noEmit",
200
- "test": "vitest run"
210
+ "test": "vitest run",
211
+ "test:corpus": "node scripts/fetch-psd-corpus.mjs && PSD_CORPUS=1 vitest run tests/psd-corpus.test.ts"
201
212
  }
202
213
  }
@@ -1,15 +1,20 @@
1
1
  ---
2
2
  name: pixerate-image-editor
3
- description: How to colour-correct and retouch a photo in the Pixerate image editor through its agent tools - base adjustments (exposure, contrast, white balance temperature and tint, saturation, blur), non-destructive adjustment layers (curves, levels, hue/saturation, vibrance, color balance, black and white, photo filter, invert, posterize, threshold) with blend modes and opacity, selections (rectangle, ellipse, magic wand, color range, grow, shrink, feather, refine edge) and layer masks, crop, rotate and flip, undo, reading the histogram for clipping, and rendering a PNG preview to check the result. Use when asked to edit, grade, correct, brighten, warm up, desaturate, crop, straighten or mask an image, or when get_adjustments, set_adjustments, add_adjustment_layer, select_by_color or render_preview are available.
3
+ description: How to colour-correct, composite and retouch a photo in the Pixerate image editor through its agent tools - layers (pixel, adjustment, group and annotation layers with masks, clipping, blend modes, opacity and fill; merge, flatten, layer via copy, develop and blur filters), base adjustments (exposure, contrast, white balance temperature and tint, saturation, blur), non-destructive adjustment layers (curves, levels, hue/saturation, vibrance, color balance, black and white, photo filter, invert, posterize, threshold) with blend modes and opacity, selections (wand, color range, grow, shrink, feather, refine edge) and layer masks, crop, rotate and flip, undo, PSD open and save, the histogram, and PNG previews. Use when asked to edit, grade, correct, brighten, warm up, desaturate, crop, straighten or mask an image, or to open or save a PSD, or when get_adjustments, set_adjustments, add_adjustment_layer, list_layers, paint_stroke, select_by_color, import_psd or render_preview are available.
4
4
  ---
5
5
 
6
6
  # Image editor
7
7
 
8
- The image editor holds one source image plus a stack of edits that never touch the source
9
- pixels: base adjustments (one value per control id), adjustment layers applied bottom to top
10
- on top of the base, an optional selection, crop/rotate/flip and annotations. Rendering order:
11
- base adjustments, then layers bottom to top (each through its mask, opacity and blend mode),
12
- then blur. See `pixerate-agent-tools` for errors, namespacing and command batching.
8
+ The image editor holds a layer stack over a canvas the size of the source image. Opening an
9
+ image gives one pixel layer, **Background**, whose pixels are the source and whose `develop`
10
+ filter holds the base adjustments; adjustment layers sit above it (bottom to top, each through
11
+ its mask, opacity and blend mode) and every annotation is its own layer on top. `list_layers`
12
+ shows the whole stack. Edits never touch the source pixels. The base controls edit the develop
13
+ filter of the active pixel layer (the Background unless another pixel layer is active); base
14
+ `blur` blurs that layer (or, in documents saved before layers with blur and adjustment layers,
15
+ the output after the layers), and base `opacity` is the output's alpha. Also an optional
16
+ selection and crop/rotate/flip. See `pixerate-agent-tools` for errors, namespacing and command
17
+ batching.
13
18
 
14
19
  - **Control ids** for base adjustments: `brightness`, `contrast`, `exposure`, `temperature`,
15
20
  `tint`, `saturation` (all -100..100, default 0), `blur` (0..30 px) and `opacity` (0..1).
@@ -43,6 +48,54 @@ then blur. See `pixerate-agent-tools` for errors, namespacing and command batchi
43
48
  preview; read the `applied` and `clamped` lists that writes return. `undo_image` takes back a
44
49
  step that made things worse.
45
50
 
51
+ ### Working with layers
52
+
53
+ 1. **Read the stack** with `list_layers`. A photo opened in the editor has only the Background;
54
+ documents made elsewhere may have pixel layers, groups and masks. Note which layer is
55
+ `active`: `set_adjustments` base controls edit the active pixel layer's `develop` filter.
56
+ 2. **New content goes on its own layer**: `add_pixel_layer` (empty, a colour fill or a PNG at
57
+ `x`, `y`), `layer_via_copy` to lift a selection onto a layer, `duplicate_layer` before a
58
+ risky change. Name layers by intent.
59
+ 3. **Shape layers non-destructively**: masks (`add_layer_mask` from the selection, or
60
+ reveal-all / hide-all, softened with `density` / `feather`) instead of deleting pixels;
61
+ `clipped: true` (via `set_layer_properties`) to limit a layer or adjustment to the layer below;
62
+ `blend`, `opacity` and `fillOpacity` instead of baking.
63
+ 4. **Organise** with `group_layers` (pass-through by default: blend modes inside still see the
64
+ layers below; give the group a blend mode to isolate them), `move_layer` and
65
+ `offset_layer`.
66
+ 5. **Bake only when asked** (`merge_down`, `merge_visible`, `flatten_image`, `apply_layer_mask`):
67
+ they are destructive. `render_preview` with `layerId` shows one layer alone.
68
+
69
+ ### Painting
70
+
71
+ 1. **Paint on its own layer** (`add_pixel_layer`), never on the Background, unless asked: a
72
+ layer can be faded, masked, blended or deleted later. Painting the Background bakes it.
73
+ 2. **Retouch through masks**: to hide part of a layer, add a mask and paint black on it
74
+ (`paint_stroke` with `channel: "mask"`, `color: "#000000"`); paint white to bring it back.
75
+ A soft brush (`hardness` 0..0.3) gives soft transitions.
76
+ 3. **One stroke is one call and one undo step.** Send the path's points in order (a few points
77
+ per brush diameter is enough; dabs are spaced by `brush.spacing`). Use `opacity` for the
78
+ overall strength and `flow` for gradual build-up within the stroke.
79
+ 4. **Limit paint with a selection** (select first, then `paint_stroke` / `fill_pixels`), or lock
80
+ transparency (`set_layer_properties` `locks.transparency`) to paint only where the layer
81
+ already has pixels.
82
+
83
+ ### Photoshop files
84
+
85
+ 1. **Inspect before opening.** `inspect_psd` lists every layer with the type it would become and
86
+ a status: `mapped` (held exactly), `approximated` (type, smart-object, fill and shape layers
87
+ come in as their stored pixels; layer effects are not rendered; Dissolve becomes Normal) or
88
+ `dropped` (Brightness/Contrast, Exposure, Channel Mixer, Gradient Map, Selective Color and
89
+ Color Lookup adjustments). Tell the user what would be lost before `import_psd` when anything
90
+ is not `mapped`.
91
+ 2. **`import_psd` replaces the document** (one undo step). CMYK, Lab, grayscale, indexed, 16-bit
92
+ and 32-bit files are converted to 8-bit RGB, and ICC profiles are ignored; the warnings say
93
+ so. Afterwards work with `list_layers` as usual: there is no Background source image, every
94
+ pixel comes from the layers.
95
+ 3. **`export_psd` writes the stack as it is**, in the source frame: crop and rotation are not
96
+ applied, layer filters and depth masks are baked, annotation layers are rasterized (only in
97
+ a browser). Read its `warnings` and pass them on. The file comes back as base64 (`data`).
98
+
46
99
  ## Best practices
47
100
 
48
101
  - **Non-destructive first.** Prefer an adjustment layer over pushing a base slider to its
@@ -77,6 +130,10 @@ then blur. See `pixerate-agent-tools` for errors, namespacing and command batchi
77
130
  levels layer without shifting hue or saturation; `color` applies a tint without changing
78
131
  brightness; `soft-light` / `overlay` for gentle contrast.
79
132
  - **Name layers** (`name`) by intent ("Sky darken", "Warm skin"), so the stack stays readable.
133
+ - **Merge Down only onto an opaque, 100 % lower layer** when the result must look the same:
134
+ the merged layer keeps the lower layer's opacity and blend, so a faded lower layer fades the
135
+ merged content too, and a blend mode on the upper layer stops seeing what was below the lower
136
+ layer where the lower one is transparent. Check with `render_preview` after merging.
80
137
 
81
138
  ## Tool reference
82
139
 
@@ -87,9 +144,12 @@ then blur. See `pixerate-agent-tools` for errors, namespacing and command batchi
87
144
  | `get_adjustments` | Read the current state: `values` by control id, `process` version (1 or 2), and `layers` bottom to top with id, kind, params, opacity, blend, enabled and mask. Call first, and to find layer ids. |
88
145
  | `list_controls` | Get every base control (id, label, section, min, max, step, default, `requiresProcess`), each layer kind's settable controls (plus `opacity`), the blend mode ids and the `layers.<layerId>.<controlId>` path format. Call once per session before writing. |
89
146
  | `get_selection` | Read the active selection: `empty`, `bounds` (natural pixels), `area` in pixels, `percent` of the image and a description. Check it after every select or modify step and before masking: a wand that covered 90% of the image is the wrong selection. |
147
+ | `list_layers` | Read the layer stack bottom to top as a tree: id, type (`pixel`, `adjustment`, `group`, `annotation`), name, document `bounds` of pixel layers, visibility, opacity, `fillOpacity`, blend, `clipped`, locks, a `mask` summary, pixel-layer `filters` (the Background's `develop` filter is the base adjustments), adjustment `kind` and `params`, group `children`, and which layer is `active`. Call it to understand a document with more than the Background before changing it; adjustment layer ids work with every adjustment-layer tool. |
90
148
  | `get_image_info` | Read the geometry: source `image` size, `crop` box (source pixels, or null), `rotation` degrees, `flipH` / `flipV`, the `output` size after crop and rotation, the layer count and `canUndo` / `canRedo`. Call before cropping or selecting so coordinates are right. |
91
149
  | `get_histogram` | Judge exposure numerically: per channel and luminance `mean`, `p1` and `p99` (0..255), and `clipping` percent at pure black and white per channel. Reads the last render, so call `render_preview` first. `bins` (16, 32, 64 or 256) adds the counts. Cheaper than another image for checking a tonal change. |
92
- | `render_preview` | See the edited image (crop, adjustments, layers, annotations) as a PNG. `maxSide` 16..2048 px, default 512. Images cost tokens: render after a group of edits, use 512 for overall look and 1024+ to judge clipping, edges and masks. Fails in environments without rendering. |
150
+ | `render_preview` | See the edited image (crop, adjustments, layers, annotations) as a PNG, or with `layerId` one layer alone. `maxSide` 16..2048 px, default 512. Images cost tokens: render after a group of edits, use 512 for overall look and 1024+ to judge clipping, edges and masks. Fails in environments without rendering. |
151
+ | `inspect_psd` | Read a PSD / PSB (`data` base64) without opening it: size, `depth`, `colorMode`, `resolution`, every layer bottom first with `psdKind`, the `type` it would become and `status` (`mapped`, `approximated`, `dropped`) with `notes`, and the import `warnings`. Call before `import_psd`. |
152
+ | `export_psd` | Save the document as an 8-bit RGB PSD (PSB above 30000 px) with layers, groups, adjustment layers and masks. Returns `data` (base64), `size` in bytes, `psb` and `warnings` (crop or rotation not applied, filters and depth masks baked, annotations rasterized or left out). Changes nothing. |
93
153
  | `capture_view` | Inspect detail in natural image pixels (the space of `get_selection` and `select_region`, before crop and rotation): `region` `content` (whole image, default), `selection` or `rect`; small regions are enlarged up to 8× with hard pixel edges, so a 64 px rect shows real pixels. `includeOverlays: true` shades unselected pixels and outlines the crop: use it to check a mask's edge before `apply_selection_as_mask`. Returns the `rect` and `scale`. Use `render_preview` for the final framed result. |
94
154
 
95
155
  ### Write
@@ -97,9 +157,9 @@ then blur. See `pixerate-agent-tools` for errors, namespacing and command batchi
97
157
  | Tool | Use it to |
98
158
  | --- | --- |
99
159
  | `set_adjustments` | Set base controls and existing layer parameters in one undo step: `values` is a map of control id or `layers.<layerId>.<controlId>` path to number; `label` is echoed. Values are clamped to range; returns `applied` and `clamped` (and a note when `tint` is set on a process 1 edit). Unknown ids fail and change nothing. Cannot set curves points, booleans or colours. |
100
- | `add_adjustment_layer` | Add a layer on top of the stack: `kind` (curves, levels, hueSaturation, vibrance, colorBalance, blackWhite, photoFilter, invert, posterize, threshold), optional `params` merged with neutral defaults, `opacity` 0..1 (default 1), `blend` (default normal), `name`. Returns the layer (with its new id) and its stack index. The only tool that sets curves points, `colorize`, `preserveLuminosity`, a black and white `tint` or a photo filter `color`. |
160
+ | `add_adjustment_layer` | Add a layer above the other adjustment layers and below annotations (or directly above `aboveId`, or at the top of group `parentId`): `kind` (curves, levels, hueSaturation, vibrance, colorBalance, blackWhite, photoFilter, invert, posterize, threshold), optional `params` merged with neutral defaults, `opacity` 0..1 (default 1), `blend` (default normal), `name`. Returns the layer (with its new id) and its stack index. The only tool that sets curves points, `colorize`, `preserveLuminosity`, a black and white `tint` or a photo filter `color`. |
101
161
  | `select_region` | Select by `shape`: `rect` or `ellipse` (with `x`, `y`, `width`, `height` in natural pixels), `all`, `none` or `invert`. `mode` replace (default), add, subtract or intersect; `feather` px (default 0). Returns the selection summary. |
102
- | `select_by_color` | Magic wand from a seed (`x`, `y`, `tolerance` 0..255 default 32, `contiguous` default true) or Color Range for every similar pixel (`color` "#rrggbb", `fuzziness` 0..200 default 40). `sampleMerged: true` samples the adjusted image instead of the source. `mode` as above. Use it for skies, backdrops and flat-coloured objects. |
162
+ | `select_by_color` | Magic wand from a seed (`x`, `y`, `tolerance` 0..255 default 32, `contiguous` default true) or Color Range for every similar pixel (`color` "#rrggbb", `fuzziness` 0..200 default 40). `sampleMerged: true` samples the adjusted image instead of the source; `sample: "active-layer"` samples only the active pixel layer (pick it with `set_active_layer`) instead of the whole image. `mode` as above. Use it for skies, backdrops and flat-coloured objects. |
103
163
  | `modify_selection` | Grow, shrink, feather, border or smooth the selection (`op`) by `radius` px (0..500). Use shrink then feather to keep an adjustment from bleeding over an edge; smooth to remove wand speckle. Fails when nothing is selected. |
104
164
  | `refine_selection` | Refine Edge: 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 %. Use on hair, fur, foliage or after loading a coarse AI mask. |
105
165
  | `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. |
@@ -111,6 +171,32 @@ then blur. See `pixerate-agent-tools` for errors, namespacing and command batchi
111
171
  | `flip_image` | Mirror the image `horizontal` (left-right) or `vertical`. Avoid flipping images with text, logos or recognisable places. |
112
172
  | `undo_image` | Undo the last edit (one tool call is one step). Returns `canUndo` / `canRedo`. |
113
173
  | `redo_image` | Redo the edit you just undid. |
174
+ | `set_active_layer` | Make a layer active (`layerId`) and choose the paint `target` (`pixels` or `mask`). Base sliders then edit that pixel layer's develop filter. Not an undo step. Returns the layer. |
175
+ | `add_pixel_layer` | Add a pixel layer above the active layer (or at `aboveId` / `belowId` / `parentId` / `index`): empty, `color` `#rrggbb[aa]` over the canvas, or `image` (PNG data URL or base64; other formats only in a browser) at `x`, `y`; with `name`, `opacity`, `blend`. Returns the layer with its id and bounds. |
176
+ | `duplicate_layer` | Copy a layer (groups with their children) directly above it, e.g. before a destructive change. Returns the copy. |
177
+ | `remove_layer` | Delete a layer or group (refused when locked). Prefer hiding with `set_layer_properties`. Returns the remaining top-level ids. |
178
+ | `move_layer` | Reorder: give exactly one of `aboveId`, `belowId`, `parentId` (into a group, optional `index`) or `index` alone. Returns the new `parentId` and `index`. |
179
+ | `set_layer_properties` | Change `name`, `visible`, `opacity`, `fillOpacity`, `blend` (`pass-through` for groups), `clipped`, `locks` or `collapsed`. Only given fields change. Returns the layer. |
180
+ | `group_layers` | Put sibling layers (`layerIds`) into a new pass-through group at the topmost one's position. Returns the group and its children. |
181
+ | `add_group` | Add an empty group (pass-through unless `blend` is given) above the active layer or at a placement, then move layers into it with `move_layer` `parentId`. Returns the group. |
182
+ | `ungroup_layer` | Replace a group by its children (its own opacity, blend and mask are dropped). Returns the child ids. |
183
+ | `merge_down` | Composite a layer onto the pixel layer below it (masks, fill and filters baked). Destructive; see Best practices. Returns the merged layer. |
184
+ | `merge_visible` | Composite all visible top-level layers into one layer; hidden ones stay. Destructive. |
185
+ | `flatten_image` | Composite everything visible into one Background; hidden layers are discarded. Destructive; do it only when asked. |
186
+ | `offset_layer` | Move a pixel layer and its mask to `x`, `y` or by `dx`, `dy` (integers, document px). Refused when its position is locked. Returns the offset and bounds. |
187
+ | `add_layer_mask` | Mask any layer `from` `reveal-all`, `hide-all` or `selection`, with optional `density` / `feather`. Replaces an existing mask. Returns the mask summary. |
188
+ | `set_layer_mask_properties` | Change any layer mask's `density`, `feather` or `enabled` without touching its pixels. |
189
+ | `apply_layer_mask` | Bake a pixel layer's mask into its alpha and remove it. Destructive. |
190
+ | `remove_layer_mask` | Delete a mask without applying it; disable it instead to keep it. |
191
+ | `layer_via_copy` | Copy the selected pixels of a pixel layer (default the active one) to a new layer above it; `cut: true` removes them from the source. Needs a selection. Returns the new layer. |
192
+ | `rasterize_layer` | Turn an annotation layer into pixels (same id and properties; browser only) or bake a pixel layer's develop / blur filters into its pixels. Do it before painting over an annotation, or when the user wants the filters fixed. Destructive. |
193
+ | `add_layer_filter` | Add a live `develop` (brightness … tint) or `blur` (`radius`) filter to a pixel layer; one of each kind. Returns the filters with ids. |
194
+ | `update_layer_filter` | Change a filter's values (`filterId` from `list_layers`). Returns the filters. |
195
+ | `remove_layer_filter` | Remove a filter. Returns the remaining filters. |
196
+ | `paint_stroke` | Paint (`tool` brush) or erase (`tool` eraser) along `points` in document px (optional `pressure` 0..1) on a pixel layer or its mask (`channel`); `color` #rrggbb (on a mask: black hides, white reveals); `brush` `{size, hardness, spacing, angle, roundness, opacity, flow, smoothing, pressureSize, pressureOpacity, blend}`. The selection limits the paint. One undo step per call. Returns the layer, channel, changed `bounds` and dab count. |
197
+ | `fill_pixels` | Fill the selection (or the canvas without one / with `useSelection: false`) on pixels or a mask with `color`, `opacity`, `blend`. Returns the changed bounds. |
198
+ | `clear_pixels` | Remove the selected pixels (alpha to 0; a transparency-locked layer or a mask gets the background colour). Returns the changed bounds. |
199
+ | `import_psd` | Open a PSD / PSB (`data` base64, a `data:` URL is fine) as the document, replacing the layers, crop and rotation (one undo step). Returns the new `layers` tree with ids, `layerIds`, size and `warnings` naming the affected layers. Call `inspect_psd` first when losses matter. |
114
200
  | `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. |
115
201
 
116
202
  ## Pitfalls
@@ -118,6 +204,17 @@ then blur. See `pixerate-agent-tools` for errors, namespacing and command batchi
118
204
  - A layer's `kind` cannot change; to switch kinds add a new layer and remove the old one.
119
205
  Curves points, `colorize` and colours are only set when the layer is added, so get them
120
206
  right in `add_adjustment_layer` (or replace the layer).
207
+ - Layer write tools refuse locked layers with the reason: unlock with `set_layer_properties`
208
+ `locks` only when the user wants that layer changed.
209
+ - `merge_down`, `merge_visible` and `flatten_image` merge annotation layers only in a browser
210
+ (they need a canvas); elsewhere they fail and say so.
211
+ - `paint_stroke`, `fill_pixels` and `clear_pixels` need a visible pixel layer (or a layer with a
212
+ raster mask and `channel: "mask"`); adjustment, group and annotation layers can only be
213
+ painted through their masks. A layer with `locks.pixels` refuses paint.
214
+ - `export_psd` is in the source frame: a crop or rotation is not in the file (the warnings say
215
+ so). Crop with `set_crop` only for PNG / JPEG output, or flatten the look you need first.
216
+ - PSD opacity, fill and mask density come in as whole percents (Photoshop stores bytes), so a
217
+ layer at 0.8 exported and reopened is 0.8, but 0.333 becomes 0.33.
121
218
  - `get_histogram` describes the last render, not the current state: render after edits, then
122
219
  read it.
123
220
  - Crop coordinates are always in the un-rotated source; after `set_rotation` the output size
@@ -126,8 +223,11 @@ then blur. See `pixerate-agent-tools` for errors, namespacing and command batchi
126
223
  the correction needs a layer (e.g. levels or curves), not a bigger number.
127
224
  - `tint` has no effect on process 1 edits (older saved documents); the result carries a note.
128
225
  Do not keep raising it; correct the green-magenta cast with a `colorBalance` layer instead.
129
- - Base `opacity` fades the whole image, not an adjustment. Base `blur` blurs the whole output
130
- after all layers; it is not a local softening tool.
226
+ - Base `opacity` fades the whole image, not an adjustment. Base `blur` blurs a whole layer (the
227
+ Background by default, before the adjustment layers; in older documents with blur and
228
+ adjustment layers, the output after them). It is not a local softening tool.
229
+ - New adjustment layers go above the other adjustment layers and below annotations, so they
230
+ never tint arrows, labels or watermarks. `list_layers` shows where each one went.
131
231
  - `invert` and `threshold` are destructive-looking creative effects; never use them for a
132
232
  correction the user did not ask for. Posterize `levels` below ~8 bands visibly.
133
233
  - Selection coordinates ignore crop and rotation: they are source-image pixels. A rectangle in