@pixerate/editor 0.221.1 → 0.222.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.
@@ -2,7 +2,7 @@ import type { AgentTool } from '../tools/registry.cjs';
2
2
  import type { ImageEditorController } from './controller.cjs';
3
3
  /**
4
4
  * Agent tools for the image editor's layers, geometry and history: `get_image_info`,
5
- * `get_histogram`, `update_adjustment_layer`, `remove_adjustment_layer`,
5
+ * `get_histogram`, `measure_region`, `update_adjustment_layer`, `remove_adjustment_layer`,
6
6
  * `move_adjustment_layer`, `set_crop`, `reset_crop`, `set_rotation`, `flip_image`,
7
7
  * `set_bit_depth`, `undo_image` and `redo_image`. Each write is one undo step and returns what changed.
8
8
  */
@@ -2,7 +2,7 @@ import type { AgentTool } from '../tools/registry.js';
2
2
  import type { ImageEditorController } from './controller.js';
3
3
  /**
4
4
  * Agent tools for the image editor's layers, geometry and history: `get_image_info`,
5
- * `get_histogram`, `update_adjustment_layer`, `remove_adjustment_layer`,
5
+ * `get_histogram`, `measure_region`, `update_adjustment_layer`, `remove_adjustment_layer`,
6
6
  * `move_adjustment_layer`, `set_crop`, `reset_crop`, `set_rotation`, `flip_image`,
7
7
  * `set_bit_depth`, `undo_image` and `redo_image`. Each write is one undo step and returns what changed.
8
8
  */
@@ -15325,10 +15325,43 @@ function createImageEditTools(controller) {
15325
15325
  annotations: { title: "Read the histogram", ...READ },
15326
15326
  handler: ({ bins }) => {
15327
15327
  const h = controller.getHistogram();
15328
- if (!h) throw new Error("No histogram yet: it comes from the rendered preview. Call render_preview, then try again.");
15328
+ if (!h) throw new Error("No histogram yet: it comes from the rendered preview. Call render_preview, then try again, or measure_region, which needs no render.");
15329
15329
  return histogramSummary(h, bins);
15330
15330
  }
15331
15331
  };
15332
+ const measureRegion = {
15333
+ name: "measure_region",
15334
+ title: "Measure part of the image",
15335
+ description: "Measures the composited image (every layer and adjustment, before crop, rotation and output opacity) in a rectangle, without rendering: the histogram summary of get_histogram (per channel and luminance mean, 1st / 99th percentiles 0-255, clipping percentages) plus the mean colour as hex and a cast (red minus blue, green minus the red-blue average, in levels; near 0 is neutral). Use it to check a correction in numbers: a known-neutral area (a white wall, a grey card) should have a cast within about \xB14; skin, sky or a product colour should land where you aim. Works where render_preview cannot. Annotation layers are left out.",
15336
+ inputSchema: {
15337
+ type: "object",
15338
+ properties: {
15339
+ x: { type: "integer", minimum: 0, description: "Left edge in natural image pixels (default: the crop, else the whole image)." },
15340
+ y: { type: "integer", minimum: 0, description: "Top edge in natural image pixels." },
15341
+ width: { type: "integer", minimum: 1, description: "Width in pixels." },
15342
+ height: { type: "integer", minimum: 1, description: "Height in pixels." }
15343
+ },
15344
+ additionalProperties: false
15345
+ },
15346
+ annotations: { title: "Measure part of the image", ...READ },
15347
+ handler: ({ x, y, width, height }) => {
15348
+ const given = [x, y, width, height].filter((v) => v !== void 0).length;
15349
+ if (given !== 0 && given !== 4) throw new Error("Give x, y, width and height together, or none to measure the crop (or the whole image).");
15350
+ const { width: W, height: H } = controller.getState().imageDimensions;
15351
+ const rect3 = given ? { x, y, width, height } : _nullishCoalesce(controller.getState().crop, () => ( { x: 0, y: 0, width: W, height: H }));
15352
+ if (rect3.x + rect3.width > W || rect3.y + rect3.height > H) throw new Error(`The rectangle runs past the image (${W} \xD7 ${H} px); keep x + width \u2264 ${W} and y + height \u2264 ${H}.`);
15353
+ const h = computeHistogram(controller.compositePixels(rect3));
15354
+ const summary3 = histogramSummary(h, void 0);
15355
+ const mean = (bins) => {
15356
+ let sum = 0;
15357
+ for (let i = 0; i < bins.length; i++) sum += i * bins[i];
15358
+ return h.samples ? sum / h.samples : 0;
15359
+ };
15360
+ const [r, g, b] = [mean(h.red), mean(h.green), mean(h.blue)];
15361
+ const hex = "#" + [r, g, b].map((v) => Math.round(v).toString(16).padStart(2, "0")).join("");
15362
+ return { rect: rect3, ...summary3, meanColor: h.samples ? hex : null, cast: { redBlue: round(r - b, 1), greenMagenta: round(g - (r + b) / 2, 1) } };
15363
+ }
15364
+ };
15332
15365
  const updateLayer = {
15333
15366
  name: "update_adjustment_layer",
15334
15367
  title: "Change an adjustment layer",
@@ -15485,7 +15518,7 @@ function createImageEditTools(controller) {
15485
15518
  return history();
15486
15519
  }
15487
15520
  };
15488
- return [getImageInfo, getHistogram, updateLayer, removeLayer2, moveLayer2, setCrop, resetCrop, setRotation, flipImage, setBitDepth, undo, redo];
15521
+ return [getImageInfo, getHistogram, measureRegion, updateLayer, removeLayer2, moveLayer2, setCrop, resetCrop, setRotation, flipImage, setBitDepth, undo, redo];
15489
15522
  }
15490
15523
 
15491
15524
  // src/image-editor/layer-tools.ts
@@ -15325,10 +15325,43 @@ function createImageEditTools(controller) {
15325
15325
  annotations: { title: "Read the histogram", ...READ },
15326
15326
  handler: ({ bins }) => {
15327
15327
  const h = controller.getHistogram();
15328
- if (!h) throw new Error("No histogram yet: it comes from the rendered preview. Call render_preview, then try again.");
15328
+ if (!h) throw new Error("No histogram yet: it comes from the rendered preview. Call render_preview, then try again, or measure_region, which needs no render.");
15329
15329
  return histogramSummary(h, bins);
15330
15330
  }
15331
15331
  };
15332
+ const measureRegion = {
15333
+ name: "measure_region",
15334
+ title: "Measure part of the image",
15335
+ description: "Measures the composited image (every layer and adjustment, before crop, rotation and output opacity) in a rectangle, without rendering: the histogram summary of get_histogram (per channel and luminance mean, 1st / 99th percentiles 0-255, clipping percentages) plus the mean colour as hex and a cast (red minus blue, green minus the red-blue average, in levels; near 0 is neutral). Use it to check a correction in numbers: a known-neutral area (a white wall, a grey card) should have a cast within about \xB14; skin, sky or a product colour should land where you aim. Works where render_preview cannot. Annotation layers are left out.",
15336
+ inputSchema: {
15337
+ type: "object",
15338
+ properties: {
15339
+ x: { type: "integer", minimum: 0, description: "Left edge in natural image pixels (default: the crop, else the whole image)." },
15340
+ y: { type: "integer", minimum: 0, description: "Top edge in natural image pixels." },
15341
+ width: { type: "integer", minimum: 1, description: "Width in pixels." },
15342
+ height: { type: "integer", minimum: 1, description: "Height in pixels." }
15343
+ },
15344
+ additionalProperties: false
15345
+ },
15346
+ annotations: { title: "Measure part of the image", ...READ },
15347
+ handler: ({ x, y, width, height }) => {
15348
+ const given = [x, y, width, height].filter((v) => v !== void 0).length;
15349
+ if (given !== 0 && given !== 4) throw new Error("Give x, y, width and height together, or none to measure the crop (or the whole image).");
15350
+ const { width: W, height: H } = controller.getState().imageDimensions;
15351
+ const rect3 = given ? { x, y, width, height } : controller.getState().crop ?? { x: 0, y: 0, width: W, height: H };
15352
+ if (rect3.x + rect3.width > W || rect3.y + rect3.height > H) throw new Error(`The rectangle runs past the image (${W} \xD7 ${H} px); keep x + width \u2264 ${W} and y + height \u2264 ${H}.`);
15353
+ const h = computeHistogram(controller.compositePixels(rect3));
15354
+ const summary3 = histogramSummary(h, void 0);
15355
+ const mean = (bins) => {
15356
+ let sum = 0;
15357
+ for (let i = 0; i < bins.length; i++) sum += i * bins[i];
15358
+ return h.samples ? sum / h.samples : 0;
15359
+ };
15360
+ const [r, g, b] = [mean(h.red), mean(h.green), mean(h.blue)];
15361
+ const hex = "#" + [r, g, b].map((v) => Math.round(v).toString(16).padStart(2, "0")).join("");
15362
+ return { rect: rect3, ...summary3, meanColor: h.samples ? hex : null, cast: { redBlue: round(r - b, 1), greenMagenta: round(g - (r + b) / 2, 1) } };
15363
+ }
15364
+ };
15332
15365
  const updateLayer = {
15333
15366
  name: "update_adjustment_layer",
15334
15367
  title: "Change an adjustment layer",
@@ -15485,7 +15518,7 @@ function createImageEditTools(controller) {
15485
15518
  return history();
15486
15519
  }
15487
15520
  };
15488
- return [getImageInfo, getHistogram, updateLayer, removeLayer2, moveLayer2, setCrop, resetCrop, setRotation, flipImage, setBitDepth, undo, redo];
15521
+ return [getImageInfo, getHistogram, measureRegion, updateLayer, removeLayer2, moveLayer2, setCrop, resetCrop, setRotation, flipImage, setBitDepth, undo, redo];
15489
15522
  }
15490
15523
 
15491
15524
  // src/image-editor/layer-tools.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixerate/editor",
3
- "version": "0.221.1",
3
+ "version": "0.222.0",
4
4
  "description": "UI-agnostic rich text and media editing core library",
5
5
  "author": "Pixerate",
6
6
  "license": "MIT",
package/skills/README.md CHANGED
@@ -40,6 +40,9 @@ request like "clean up this export" finds it.
40
40
  | Skill | Editor | Covers |
41
41
  | --- | --- | --- |
42
42
  | [`pixerate-image-selection-masking`](pixerate-image-selection-masking/SKILL.md) | Image (`pixerate-image-editor`) | Cutouts, background and sky replacement, local adjustments through masks; choosing a selection method, checking and refining edges, and outputting an editable mask. |
43
+ | [`pixerate-image-photo-correction`](pixerate-image-photo-correction/SKILL.md) | Image (`pixerate-image-editor`) | Global correction in order: straighten and crop, exposure and white balance, points and contrast, colour, local, look; each checked for clipping and neutral greys. |
44
+ | [`pixerate-image-portrait-retouch`](pixerate-image-portrait-retouch/SKILL.md) | Image (`pixerate-image-editor`) | Blemishes, stray hairs and flaws healed on their own layer, dodge and burn, eyes and teeth, kept natural. |
45
+ | [`pixerate-image-product-photo`](pixerate-image-product-photo/SKILL.md) | Image (`pixerate-image-editor`) | Product cutouts on pure white or a brand colour, a grounding shadow, accurate colour, centred marketplace crops. |
43
46
  | [`pixerate-spreadsheet-clean-import`](pixerate-spreadsheet-clean-import/SKILL.md) | Spreadsheet (`pixerate-spreadsheet`) | Imported or pasted data made tidy: structure, headers, types (currency, comma decimals, day-first dates, leading zeros, missing values), text, duplicates and validation. |
44
47
  | [`pixerate-talking-head-edit`](pixerate-talking-head-edit/SKILL.md) | Video (`pixerate-video-editor`) | Talking-head cleanup by transcript: fillers, long pauses and stutters found in one pass and cut as one plan reviewed before it applies. Owns the dialogue tools. |
45
48
  | [`pixerate-bgm-alignment`](pixerate-bgm-alignment/SKILL.md) | Video (`pixerate-video-editor`) | Background music: beats and cuts on the beat, ducking under speech, loudness in LUFS. Owns the music tools. |
@@ -7,20 +7,13 @@ description: How to colour-correct, composite and retouch a photo in the Pixerat
7
7
 
8
8
  The image editor holds a layer stack over a canvas the size of the source image. Opening an
9
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 (pen, rectangle,
12
- ellipse, line and arrow annotations are shape layers, text annotations type layers, that remember
13
- them). `list_layers`
14
- shows the whole stack. Edits never touch the source pixels. The base controls edit the develop
15
- filter of the active pixel layer (the Background unless another pixel layer is active); base
16
- `blur` blurs that layer (or, in documents saved before layers with blur and adjustment layers,
17
- the output after the layers), and base `opacity` is the output's alpha. Pixel, annotation, group
18
- and smart object layers can carry live effects (shadows, glows, bevels, overlays, strokes). A smart
19
- object places contents (pixels or a nested layer document) through a transform and smart filters,
20
- re-rendered from the contents on every change. A shape layer holds vector paths with a fill and a
21
- stroke, rendered again from the paths on every change, so it stays sharp through any transform. A
22
- type layer holds live, styled text (point or paragraph) drawn with the editor's fonts.
23
- Also an optional selection and crop/rotate/flip. See `pixerate-agent-tools` for errors and batching.
10
+ filter holds the base adjustments (the base controls edit the develop filter of the active pixel
11
+ layer). Adjustment layers sit above it, each through its mask, opacity and blend mode, and
12
+ annotations are shape and type layers on top. Edits never touch the source pixels; `list_layers`
13
+ shows the whole stack. Pixel, group and smart object layers can carry live effects; a smart
14
+ object places contents through a transform and smart filters; shape layers hold vector paths and
15
+ type layers live text, re-rendered on every change. There is also an optional selection and a
16
+ crop, rotation and flip. See `pixerate-agent-tools` for errors and batching.
24
17
 
25
18
  - **Control ids** for base adjustments: `brightness`, `contrast`, `exposure`, `temperature`,
26
19
  `tint`, `saturation` (all -100..100, default 0), `blur` (0..30 px) and `opacity` (0..1).
@@ -38,7 +31,10 @@ Also an optional selection and crop/rotate/flip. See `pixerate-agent-tools` for
38
31
  For these jobs, load the workflow skill too: it has the method, the checks between steps and
39
32
  the pitfalls, and uses the tools below.
40
33
 
34
+ - `pixerate-image-photo-correction`: global correction in order (frame, light, colour, local, look), with clipping and neutral checks.
41
35
  - `pixerate-image-selection-masking`: cutouts, background and sky replacement, local adjustments through masks.
36
+ - `pixerate-image-portrait-retouch`: blemishes, stray hairs, dodge and burn, eyes and teeth.
37
+ - `pixerate-image-product-photo`: product on white, a grounding shadow, marketplace crops.
42
38
 
43
39
  ## Working method
44
40
 
@@ -46,19 +42,15 @@ the pitfalls, and uses the tools below.
46
42
  `get_image_info` for the image size, crop and rotation; `list_controls` once for control
47
43
  ids, ranges, layer kinds and blend modes; `render_preview` (default 512 px) to see the
48
44
  image, then `get_histogram` to judge exposure and clipping in numbers.
49
- 2. **Plan the order of corrections** (see Best practices): straighten and crop first (so
50
- later judgements are on the final frame), then global tone and white balance,
51
- then contrast and tone curves, then colour, then local fixes through masks, then creative
52
- looks.
45
+ 2. **Plan the order:** frame, light, colour, local, look (`pixerate-image-photo-correction`).
53
46
  3. **Write in few steps.** Put every base change of one correction into one `set_adjustments`
54
47
  call (one undo step) with a `label`. Create layers with their `params` in the same
55
48
  `add_adjustment_layer` call rather than adding a neutral layer and then setting it.
56
49
  4. **Local edits:** select, refine, check with `get_selection`, then `apply_selection_as_mask`
57
50
  with a `kind`. Load `pixerate-image-selection-masking` for cutouts, masks and local edits.
58
- 5. **Verify.** `render_preview` after each group of corrections, not after each slider, then
59
- `get_histogram` (it reads the last render) for clipping. Compare against the previous
60
- preview; read the `applied` and `clamped` lists that writes return. `undo_image` takes back a
61
- step that made things worse.
51
+ 5. **Verify** after each group of corrections: `measure_region` (no render needed) or
52
+ `render_preview` then `get_histogram`; read the `applied` and `clamped` lists writes return.
53
+ `undo_image` takes back a step that made things worse.
62
54
 
63
55
  ### Working with layers
64
56
 
@@ -92,12 +84,9 @@ filters, so nothing degrades. Read [references/smart-objects.md](references/smar
92
84
 
93
85
  ### Shapes and text
94
86
 
95
- Draw boxes, badges, arrows and callouts as shape layers (`add_shape_layer` with a live `shape`,
96
- then `set_shape_component` / `update_shape` / `add_shape_component`), and text as type layers
97
- (`add_text_layer`, then `set_text`, `set_text_style`, `set_paragraph_style` by `start` / `end`
98
- range). Both stay sharp through transforms and take effects, masks and blend modes; rasterize only
99
- to paint on them. Read [references/shapes-and-type.md](references/shapes-and-type.md) before
100
- drawing or editing either.
87
+ Boxes, badges, arrows and callouts are shape layers (`add_shape_layer`), text is type layers
88
+ (`add_text_layer`); both stay sharp and take effects, masks and blends. Read
89
+ [references/shapes-and-type.md](references/shapes-and-type.md) before drawing or editing either.
101
90
 
102
91
  ### Layer effects
103
92
 
@@ -117,12 +106,11 @@ selection or locked transparency. Read [references/painting.md](references/paint
117
106
  ### Colour management
118
107
 
119
108
  Working space, profiles, soft proofing and print conversions: read [references/color-management.md](references/color-management.md).
109
+
120
110
  ### Retouching
121
111
 
122
- Clone Stamp, Healing Brush, Spot Healing, Smudge, Dodge, Burn, Sponge, Blur, Sharpen and the
123
- History Brush: retouch on an empty layer with `sample: "current-below"`, pick `spot_heal` for small
124
- spots, `heal_brush` for larger flaws and `clone_stamp` to copy exactly (`set_clone_transform` scales,
125
- rotates or flips the source), and keep strokes short and soft. Read [references/retouching.md](references/retouching.md) first.
112
+ Heal, clone, dodge and burn on layers above the photo (`sample: "current-below"`): load
113
+ `pixerate-image-portrait-retouch`, and read [references/retouching.md](references/retouching.md) first.
126
114
 
127
115
  ### Photoshop files
128
116
 
@@ -135,27 +123,8 @@ in the source frame at the document's bit depth, colour mode and profile. Read
135
123
  - **Non-destructive first.** Prefer an adjustment layer over pushing a base slider to its
136
124
  limit: layers can be masked, faded with `opacity`, blended and stacked. Use base sliders for
137
125
  global exposure and white balance, layers for everything you may want to tune later.
138
- - **Order of corrections** (as in a raw / Photoshop workflow): exposure and white balance
139
- (`exposure`, `temperature`, `tint`) → black and white points and contrast (a `levels` layer,
140
- then a gentle S-curve `curves` layer) → colour (`vibrance` before `saturation`,
141
- `hueSaturation` per range, `colorBalance`) → local adjustments through masks → creative looks
142
- (`photoFilter`, `blackWhite`, `posterize`). Correct before you stylise.
143
- - **Avoid clipping.** Set levels black and white points (`master.inBlack`, `master.inWhite`)
144
- just inside where the image data starts (the histogram's `p1` / `p99`), not past it; large
145
- `exposure` or `contrast` values blow highlights and crush shadows in the output. More than
146
- about 0.5% of pixels in `clipping.highlightsPercent` or `shadowsPercent` on a normal photo
147
- means detail is lost; check bright skies, skin and deep shadows in `render_preview` too.
148
- - **Straighten, then crop.** Level horizons with `set_rotation` (small angles such as -2.5);
149
- any angle that is not a quarter turn leaves transparent corners, so crop inside them with
150
- `set_crop` afterwards. Keep a standard aspect ratio (3:2, 4:3, 16:9, 1:1, 4:5) unless asked
151
- otherwise, and do not crop away the subject's space to look or move into.
152
- - **Small moves.** Real corrections are usually within ±10..30 on these scales. Prefer
153
- `vibrance` over `saturation` for portraits (it protects already-saturated colours and skin).
154
- - **Contrast through curves.** A curves S-curve keeps endpoints fixed and avoids the flat
155
- greys that a big `contrast` value with low `exposure` produces. Curve points are levels
156
- 0..255 (`{ input, output }`); keep `(0,0)` and `(255,255)` and move quarter-tones by 10..25.
157
- - **Neutral greys.** Use `temperature` / `tint` (or `colorBalance` midtones) so known-neutral
158
- areas (white walls, grey roads, clouds) have no cast; then add warmth deliberately if wanted.
126
+ - **Correct in order** (frame, light, colour, local, look) and measure clipping and neutrals after
127
+ each step: `pixerate-image-photo-correction` has the method and settings.
159
128
  - **Masks:** select, refine, then mask; see `pixerate-image-selection-masking`.
160
129
  - **Blend modes for tone-only or colour-only changes:** `luminosity` applies a curves or
161
130
  levels layer without shifting hue or saturation; `color` applies a tint without changing
@@ -166,29 +135,8 @@ in the source frame at the document's bit depth, colour mode and profile. Read
166
135
  natural colour mixing and painterly strokes; it is not a Photoshop mode, so keep it off
167
136
  layers that must survive a PSD round trip. It costs about ten times a normal blend.
168
137
  - **Name layers** (`name`) by intent ("Sky darken", "Warm skin"), so the stack stays readable.
169
- - **Effects: subtle first.** A drop shadow reads as natural with a small `distance` (2..10 px),
170
- a `size` a little larger than the distance, `opacity` 0.3..0.6 and the default multiply; a
171
- hard, dark, far shadow looks pasted on. Glows want `screen` and a light colour; strokes on
172
- text 1..4 px. Bevels: `depth` 1..2 and `size` a fraction of the shape's thickness.
173
- - **Outline-only looks** (an outlined title, a glass button): set the layer's `fillOpacity` to
174
- 0 with `set_layer_properties` and keep its effects; fill opacity hides the layer's own pixels,
175
- layer `opacity` fades the effects too.
176
- - **Prefer an effect over painted pixels** for shadows, glows, outlines and colour fills of a
177
- shape: it follows later edits and stays adjustable.
178
- - **Transforms: one resample, bicubic, whole pixels when you can.** Combine a scale, rotation and
179
- move into one `transform_layer` call; use `rotate_layer_90` / `flip_layer` (exact) instead of
180
- `rotate: 90` or `scaleX: -100`. `interpolation: "nearest"` keeps hard pixel art crisp; bicubic
181
- (default) is right for photos, bilinear is a little softer. Enlarging past about 200 % shows
182
- softness: prefer a larger source.
183
- - **Scale proportionally** unless asked to stretch: give `scaleX` and `scaleY` the same value.
184
- Keep the subject in frame: after a scale about `center` the layer can run past the canvas,
185
- which `bounds` in the result shows.
186
- - **Smart objects for anything you may scale again.** Convert first, then transform as often as
187
- needed; set exact sizes with `set_smart_object_transform` `scaleX` / `scaleY` (percent of the
188
- contents) rather than chaining relative scales. Prefer smart filters over layer filters when
189
- the user may want to mask, blend or reorder them later.
190
- - **Linked or not**: duplicate a smart object `linked: true` for repeated elements that must stay
191
- identical (a logo placed twice); use `linked: false` when the copies will be edited apart.
138
+ - **Effects, transforms and smart objects** have their own best practices in
139
+ `references/layer-effects.md`, `references/free-transform.md` and `references/smart-objects.md`.
192
140
  - **Merge Down only onto an opaque, 100 % lower layer** when the result must look the same:
193
141
  the merged layer keeps the lower layer's opacity and blend, so a faded lower layer fades the
194
142
  merged content too, and a blend mode on the upper layer stops seeing what was below the lower
@@ -216,6 +164,7 @@ in the source frame at the document's bit depth, colour mode and profile. Read
216
164
  | `list_layer_effects` | Read one layer's effects: `enabled` (master switch), `scale` (Scale Effects), `margin` (px they reach beyond the shape), `fromPsd`, and every effect with `id`, `kind`, `label` and all its settings. Also returns the `globalLight`, the `patterns` pattern effects can use and the effect `kinds`. Call before changing effects you did not add. |
217
165
  | `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, `bitDepth` (8, 16 or 32), the layer count and `canUndo` / `canRedo`. Call before cropping or selecting so coordinates are right. |
218
166
  | `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. |
167
+ | `measure_region` | Measure a rectangle of the composited image (`x`, `y`, `width`, `height` in natural pixels; default the crop, else the whole image) without a render: the `get_histogram` summary plus `meanColor` (hex) and `cast` (`redBlue`, `greenMagenta` in levels; within about ±4 is neutral). Check a neutral reference, skin, a background or clipping after each correction. |
219
168
  | `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. |
220
169
  | `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 (`smart`, `shape` and `type` for Photoshop smart objects, shapes and text) and `status` (`mapped`, `approximated`, `dropped`) with `notes`, and the import `warnings`. Call before `import_psd`. |
221
170
  | `export_psd` | Save the document as an RGB PSD at its bit depth (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, Pigment blends written as Normal). Shape and type layers are written as real Photoshop shape and type layers. Changes nothing. |
@@ -20,3 +20,14 @@ How to move, scale, rotate, skew, distort and warp layers, selected pixels and m
20
20
  4. **Transform once.** Every call resamples the pixels, so chained calls soften them: put scale,
21
21
  rotation and move into one `transform_layer` call (or one matrix) instead of several.
22
22
  5. **Look**: `render_preview`, or `capture_view` at the edges; each call is one undo step.
23
+
24
+ ## Best practices
25
+
26
+ - **Transforms: one resample, bicubic, whole pixels when you can.** Combine a scale, rotation and
27
+ move into one `transform_layer` call; use `rotate_layer_90` / `flip_layer` (exact) instead of
28
+ `rotate: 90` or `scaleX: -100`. `interpolation: "nearest"` keeps hard pixel art crisp; bicubic
29
+ (default) is right for photos, bilinear is a little softer. Enlarging past about 200 % shows
30
+ softness: prefer a larger source.
31
+ - **Scale proportionally** unless asked to stretch: give `scaleX` and `scaleY` the same value.
32
+ Keep the subject in frame: after a scale about `center` the layer can run past the canvas,
33
+ which `bounds` in the result shows.
@@ -22,3 +22,15 @@ Read this before `add_layer_effect`, `update_layer_effect`, `set_global_light` o
22
22
  every effect of a layer (each keeps its own `enabled`); `scale` resizes them all (Scale
23
23
  Effects), e.g. after resizing a layer's content.
24
24
  5. **Look after each change**: `render_preview` (with `layerId` for one layer alone).
25
+
26
+ ## Best practices
27
+
28
+ - **Effects: subtle first.** A drop shadow reads as natural with a small `distance` (2..10 px),
29
+ a `size` a little larger than the distance, `opacity` 0.3..0.6 and the default multiply; a
30
+ hard, dark, far shadow looks pasted on. Glows want `screen` and a light colour; strokes on
31
+ text 1..4 px. Bevels: `depth` 1..2 and `size` a fraction of the shape's thickness.
32
+ - **Outline-only looks** (an outlined title, a glass button): set the layer's `fillOpacity` to
33
+ 0 with `set_layer_properties` and keep its effects; fill opacity hides the layer's own pixels,
34
+ layer `opacity` fades the effects too.
35
+ - **Prefer an effect over painted pixels** for shadows, glows, outlines and colour fills of a
36
+ shape: it follows later edits and stays adjustable.
@@ -26,3 +26,12 @@ How to work with smart objects in the image editor (see the main skill for the t
26
26
  in a new image for every linked instance; `export_smart_object_contents` returns the file.
27
27
  6. **Finish**: `rasterize_smart_object` only when the user wants plain pixels to paint on; the
28
28
  placement and filters are then gone.
29
+
30
+ ## Best practices
31
+
32
+ - **Smart objects for anything you may scale again.** Convert first, then transform as often as
33
+ needed; set exact sizes with `set_smart_object_transform` `scaleX` / `scaleY` (percent of the
34
+ contents) rather than chaining relative scales. Prefer smart filters over layer filters when
35
+ the user may want to mask, blend or reorder them later.
36
+ - **Linked or not**: duplicate a smart object `linked: true` for repeated elements that must stay
37
+ identical (a logo placed twice); use `linked: false` when the copies will be edited apart.
@@ -0,0 +1,129 @@
1
+ ---
2
+ name: pixerate-image-photo-correction
3
+ description: The global photo correction workflow in the Pixerate image editor - straightening and cropping, then exposure and white balance, black and white points, contrast and tone curves, colour and saturation, local fixes and finally a look, each checked in numbers (histogram, clipping, neutral greys) before the next. Use when asked to fix, correct, enhance, edit, brighten, darken, warm up, cool down or colour-correct a photo, remove a colour cast, recover shadows or highlights, add contrast or punch, straighten a horizon or crop to a ratio, or when set_adjustments, add_adjustment_layer, get_histogram or measure_region are available. Load with pixerate-image-editor, which lists every tool.
4
+ metadata:
5
+ kind: workflow
6
+ reference: pixerate-image-editor
7
+ ---
8
+
9
+ # Correcting a photo
10
+
11
+ A correction is a sequence where each step changes what the next one sees, so the order
12
+ matters: frame, then light, then colour, then local, then style. The base controls
13
+ (`set_adjustments`) edit the Background's develop filter; adjustment layers stack above it and
14
+ can be masked, faded and blended later. Judge with numbers as well as eyes: `measure_region`
15
+ measures any rectangle of the composited image without a render (mean colour, cast, percentiles,
16
+ clipping), and `get_histogram` summarises the last `render_preview`.
17
+
18
+ This skill is the method. `pixerate-image-editor` has the full tool reference and control ids.
19
+
20
+ ## Goal and quality bar
21
+
22
+ - Nothing important clips: under about 0.5 % of pixels at pure black or white per channel on a
23
+ normal photo (`clipping` in `measure_region` or `get_histogram`), unless a light source or a
24
+ specular highlight is meant to.
25
+ - Things that should be neutral are: a white wall, a grey card or clouds within about ±4 levels
26
+ of cast (`cast.redBlue`, `cast.greenMagenta`).
27
+ - The tones span the range the scene has: shadows near but above 0, highlights near but below
28
+ 255 (`stats.luminance.p1` / `p99`), midtones where the subject reads well.
29
+ - Every correction is still adjustable: base sliders for global light and white balance, layers
30
+ for the rest, named by intent. Nothing is merged or flattened unless asked.
31
+
32
+ ## Inputs to confirm
33
+
34
+ - **The intent:** a faithful correction (the default) or a look (warm, moody, high contrast).
35
+ - **A neutral reference** if there is one: a grey card, white paper, a wall the user knows is
36
+ white. Ask when the scene has none and the "right" colour is a matter of taste.
37
+ - **Output ratio** if the user mentions a destination (Instagram 4:5, a print size, 16:9).
38
+
39
+ ## Working method
40
+
41
+ 1. **Read.** `get_image_info` (size, crop, rotation), `get_adjustments` (what is already set),
42
+ `list_layers`, then `measure_region` with no rectangle (the whole frame) and on the subject.
43
+ **Check:** you know where the image clips, its overall cast and where the subject is.
44
+ 2. **Straighten and crop first**, so every later judgement is on the final frame. Level a
45
+ horizon or vertical with `set_rotation` (small angles, -3..3), then `set_crop` inside the
46
+ transparent corners at the asked ratio; keep the subject's space to look or move into.
47
+ **Check:** `get_image_info` `crop` has the ratio you meant and contains the subject.
48
+ 3. **Exposure and white balance** with one `set_adjustments` call (`exposure`, `temperature`,
49
+ `tint`, with a `label`). Set white balance from the neutral reference: `measure_region` on
50
+ it, move `temperature` against `cast.redBlue` and `tint` against `cast.greenMagenta`, measure
51
+ again. For a precise fix, a `levels` layer with per-channel `inWhite` scales each channel so
52
+ the reference comes out grey (scale each channel to the same level).
53
+ **Check:** the reference's `cast` is within ±4; the frame does not clip.
54
+ 4. **Black and white points, then contrast.** A `levels` layer with `master.inBlack` /
55
+ `master.inWhite` just inside where the data starts (`p1` / `p99`), never past it. Then a
56
+ gentle S-curve `curves` layer (quarter-tones moved 10..25 levels, endpoints fixed). To lift
57
+ dark areas without touching bright ones, raise `master.gamma` (1.2..1.8) instead of exposure.
58
+ **Check:** `clipping` stays under 0.5 % per channel; shadows and highlights still have detail.
59
+ 5. **Colour.** `vibrance` before `saturation` (it spares skin and colours already strong),
60
+ `hueSaturation` per range for one colour, `colorBalance` for a cast left in the shadows or
61
+ highlights. Small moves: real corrections are ±10..30.
62
+ **Check:** neutrals still neutral; skin has red above green above blue and no orange or magenta.
63
+ 6. **Local fixes** through masks: a sky darkened, a face lifted. Select (see
64
+ `pixerate-image-selection-masking`), then `add_adjustment_layer` while the selection is
65
+ active, or `apply_selection_as_mask` with a `kind`.
66
+ **Check:** `measure_region` inside and just outside the mask: the change stops where it should.
67
+ 7. **Look last**, if asked: `photoFilter`, `blackWhite`, a split tone with `colorBalance`, at
68
+ reduced `opacity`. Correct before you stylise.
69
+ 8. **Verify the whole.** `measure_region` on the frame and the reference again; `render_preview`
70
+ and `get_histogram` where a canvas is available to look as well as measure. `undo_image` any
71
+ step that made it worse, and report the settings you used.
72
+
73
+ ## Decision table
74
+
75
+ | Problem | Fix |
76
+ | --- | --- |
77
+ | Whole image too dark or bright, nothing clipping | `exposure` (base), ±10..30 |
78
+ | Dark shadows, bright sky or window | `levels` `master.gamma` 1.2..1.8, or a curves lift in the lower quarter; never exposure, which clips the sky |
79
+ | Bright areas blown, shadows fine | Lower the top of a curve, or `levels` `master.outWhite` a little; clipped detail cannot come back |
80
+ | Flat, grey, low contrast | `levels` points at `p1` / `p99`, then a gentle S-curve |
81
+ | Colour cast, with a neutral reference | Per-channel `levels` `inWhite` (precise), or `temperature` / `tint` (quick) |
82
+ | Colour cast, no reference | `temperature` / `tint` until whites and clouds look clean; ask whether warmth is wanted |
83
+ | Cast only in the shadows or highlights | `colorBalance` shadows or highlights |
84
+ | Dull colour | `vibrance` +10..25; `saturation` only for non-portrait scenes |
85
+ | One colour wrong (green grass too yellow, sky too cyan) | `hueSaturation` on that range |
86
+ | Contrast change shifts colours | Put the curves or levels layer in `luminosity` blend (`set_layer_properties`) |
87
+ | Tilted horizon | `set_rotation`, then `set_crop` inside the corners |
88
+
89
+ ## Recipes
90
+
91
+ **Neutral from a grey card.** `measure_region` on the card gives mean levels per channel (from
92
+ `stats`). Pick the target (the green level, or the mean), and set each channel's `inWhite` to
93
+ `255 × channel / target` in one `add_adjustment_layer` `levels` call (`params: { red: { inWhite },
94
+ green: { inWhite }, blue: { inWhite } }`). Measure the card again, and the whole frame for clipping.
95
+
96
+ **Lift shadows, keep the sky.** `add_adjustment_layer` `levels` with `params: { master: { gamma:
97
+ 1.6 } }`; measure the sky's `clipping` and the foreground's luminance mean; raise or lower gamma.
98
+ For a lift limited to the foreground, add it while a selection of the foreground is active.
99
+
100
+ **Crop to a ratio, as large as possible.** The largest crop with ratio `w:h` inside an image of
101
+ `W × H` is `W` wide when `W / H` < `w / h`, else `H` tall; position it to keep the subject in
102
+ frame and give it room on the side it faces.
103
+
104
+ ## Pitfalls
105
+
106
+ - **Correcting in the wrong order.** Colour judged before exposure or white balance gets redone;
107
+ a crop after grading changes what the histogram meant.
108
+ - **Exposure for shadows.** Raising exposure to open shadows clips the sky; use gamma or curves.
109
+ - **Points past the data.** `inBlack` above `p1` or `inWhite` below `p99` crushes or blows
110
+ everything beyond; `clipping` shows it.
111
+ - **Saturation on skin.** It turns skin orange fast; use vibrance, and measure a skin patch.
112
+ - **Trusting the screen alone.** Measure neutrals and clipping; a preview at 512 px hides small
113
+ clipped areas.
114
+ - **Stacking corrections in base sliders.** They cannot be masked or faded; use layers for
115
+ anything beyond global exposure and white balance.
116
+ - **`get_histogram` before a render.** It reads the last `render_preview` and fails without one;
117
+ `measure_region` needs no render.
118
+
119
+ ## Tools used
120
+
121
+ - `get_image_info`, `get_adjustments`, `list_controls`, `list_layers`: read the frame, settings, control ids and stack
122
+ - `measure_region`: mean colour, cast, percentiles and clipping of any rectangle, without a render
123
+ - `render_preview`, `get_histogram`, `capture_view`: look at the result and read its histogram
124
+ - `set_rotation`, `set_crop`, `reset_crop`: straighten and frame
125
+ - `set_adjustments`: exposure, white balance and other base controls, one undo step per call
126
+ - `add_adjustment_layer`, `update_adjustment_layer`, `remove_adjustment_layer`: levels, curves, vibrance, hue and colour balance as layers
127
+ - `set_layer_properties`: blend mode (luminosity), opacity and name of a correction layer
128
+ - `apply_selection_as_mask`: a masked adjustment layer for a local fix
129
+ - `undo_image`: take back a step
@@ -0,0 +1,19 @@
1
+ {
2
+ "prompt": "Crop this photo to a 4:5 portrait for Instagram, as large as possible, keeping the whole person (the tall figure at x 50-70, y 15-70) in frame.",
3
+ "fixture": {
4
+ "editor": "image",
5
+ "width": 120,
6
+ "height": 80,
7
+ "background": [110, 140, 90],
8
+ "shapes": [{ "shape": "rect", "x": 50, "y": 15, "width": 20, "height": 55, "color": [180, 120, 100] }]
9
+ },
10
+ "checks": [
11
+ { "why": "a 4:5 portrait crop", "tool": "get_image_info", "expect": { "crop": { "$expr": "v && Math.abs(v.width / v.height - 0.8) <= 0.02" } } },
12
+ { "why": "as large as the frame allows (full height)", "tool": "get_image_info", "expect": { "crop": { "height": { "$min": 78 } } } },
13
+ { "why": "the whole person is inside", "tool": "get_image_info", "expect": { "crop": { "$expr": "v && v.x <= 50 && v.x + v.width >= 70 && v.y <= 15 && v.y + v.height >= 70" } } }
14
+ ],
15
+ "solution": [
16
+ { "tool": "get_image_info" },
17
+ { "tool": "set_crop", "args": { "x": 28, "y": 0, "width": 64, "height": 80 } }
18
+ ]
19
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "prompt": "The foreground of this landscape is far too dark, but the sky is already bright. Brighten the foreground without blowing out the sky.",
3
+ "fixture": {
4
+ "editor": "image",
5
+ "width": 120,
6
+ "height": 80,
7
+ "background": [40, 45, 40],
8
+ "shapes": [{ "shape": "rect", "x": 0, "y": 0, "width": 120, "height": 30, "color": [225, 232, 245] }]
9
+ },
10
+ "checks": [
11
+ { "why": "the foreground is lifted into the midtones", "tool": "measure_region", "args": { "x": 0, "y": 40, "width": 120, "height": 40 }, "expect": { "stats": { "luminance": { "mean": { "$min": 70, "$max": 160 } } } } },
12
+ { "why": "the sky keeps its detail: nothing clips to white", "tool": "measure_region", "args": { "x": 0, "y": 0, "width": 120, "height": 30 }, "expect": { "clipping": { "highlightsPercent": { "red": { "$max": 0.5 }, "green": { "$max": 0.5 }, "blue": { "$max": 0.5 } } } } },
13
+ { "why": "the sky stays bright", "tool": "measure_region", "args": { "x": 0, "y": 0, "width": 120, "height": 30 }, "expect": { "stats": { "luminance": { "mean": { "$min": 200 } } } } }
14
+ ],
15
+ "solution": [
16
+ { "tool": "measure_region" },
17
+ { "tool": "add_adjustment_layer", "args": { "kind": "levels", "name": "Lift shadows", "params": { "master": { "gamma": 1.8 } } } },
18
+ { "tool": "measure_region", "args": { "x": 0, "y": 0, "width": 120, "height": 30 } }
19
+ ]
20
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "prompt": "This photo has a cold blue cast. The grey card in the lower left (x 10-30, y 50-70) should be neutral grey. Fix the colour without clipping anything, and keep it adjustable.",
3
+ "fixture": {
4
+ "editor": "image",
5
+ "width": 120,
6
+ "height": 80,
7
+ "background": [50, 60, 85],
8
+ "shapes": [
9
+ { "shape": "rect", "x": 0, "y": 0, "width": 120, "height": 20, "color": [120, 135, 170] },
10
+ { "shape": "rect", "x": 10, "y": 50, "width": 20, "height": 20, "color": [70, 80, 105] }
11
+ ]
12
+ },
13
+ "checks": [
14
+ { "why": "the grey card is neutral (cast within ±4 levels)", "tool": "measure_region", "args": { "x": 12, "y": 52, "width": 16, "height": 16 }, "expect": { "cast": { "redBlue": { "$min": -4, "$max": 4 }, "greenMagenta": { "$min": -4, "$max": 4 } } } },
15
+ { "why": "the card is a mid grey, not crushed or blown", "tool": "measure_region", "args": { "x": 12, "y": 52, "width": 16, "height": 16 }, "expect": { "stats": { "luminance": { "mean": { "$min": 90, "$max": 170 } } } } },
16
+ { "why": "no channel clips anywhere", "tool": "measure_region", "expect": { "clipping": { "highlightsPercent": { "red": { "$max": 0.5 }, "green": { "$max": 0.5 }, "blue": { "$max": 0.5 } }, "shadowsPercent": { "red": { "$max": 0.5 }, "green": { "$max": 0.5 }, "blue": { "$max": 0.5 } } } } },
17
+ { "why": "the correction is a layer, not baked into the photo", "tool": "list_layers", "expect": { "layers": { "$some": { "type": "adjustment" } } } }
18
+ ],
19
+ "solution": [
20
+ { "tool": "measure_region", "args": { "x": 12, "y": 52, "width": 16, "height": 16 } },
21
+ { "tool": "add_adjustment_layer", "args": { "kind": "levels", "name": "Neutralise cast", "params": { "red": { "inWhite": 140 }, "green": { "inWhite": 159 }, "blue": { "inWhite": 209 } } } },
22
+ { "tool": "measure_region", "args": { "x": 12, "y": 52, "width": 16, "height": 16 } },
23
+ { "tool": "measure_region" }
24
+ ]
25
+ }
@@ -0,0 +1,115 @@
1
+ ---
2
+ name: pixerate-image-portrait-retouch
3
+ description: The portrait retouching workflow in the Pixerate image editor - removing blemishes, spots and stray hairs, healing larger flaws, evening skin while keeping its texture, dodging and burning, brightening eyes and teeth, and keeping the result natural and reversible on separate layers. Use when asked to retouch, clean up, airbrush or touch up a portrait, headshot or face, remove spots, pimples, acne, wrinkles, stray hairs or dust, smooth or even skin, brighten eyes or whiten teeth, or when spot_heal, heal_brush, clone_stamp, dodge or burn are available. Load with pixerate-image-editor, which lists every tool.
4
+ metadata:
5
+ kind: workflow
6
+ reference: pixerate-image-editor
7
+ ---
8
+
9
+ # Retouching a portrait
10
+
11
+ Good retouching removes what is temporary (a spot, a stray hair, a shine) and keeps what makes
12
+ the person look like themselves (moles they keep, freckles, pores, laughter lines). Work from
13
+ the global to the small, on layers above the photo, so every step can be faded or taken back.
14
+ The healing tools paint onto an empty layer when they sample `current-below`; tonal tools
15
+ (dodge, burn, sponge, smudge) change the pixels they touch, so they work on a duplicate.
16
+
17
+ This skill is the method. `pixerate-image-editor` has the tool reference, and its
18
+ `references/retouching.md` the detail of each retouching tool.
19
+
20
+ ## Goal and quality bar
21
+
22
+ - Temporary flaws are gone: a blemish area measures like the skin around it (`measure_region`
23
+ on a small box around it: luminance `p1` close to the surrounding skin).
24
+ - Skin keeps its texture: no flat, plastic patches. Heal and clone copy real texture; blur only
25
+ locally and lightly.
26
+ - Tone and colour are natural: skin has red above green above blue, eyes and teeth brightened
27
+ only a little (they stay off-white).
28
+ - The photo layer is unchanged: retouching lives on named layers ("Retouch", "Dodge & burn",
29
+ "Eyes") the user can fade or switch off.
30
+
31
+ ## Inputs to confirm
32
+
33
+ - **How much:** a light clean-up (temporary flaws only, the default), a polished commercial
34
+ retouch, or something specific.
35
+ - **What to keep:** moles, freckles, scars and features that are part of the person stay unless
36
+ the user asks.
37
+ - **Where:** the flaws to fix when the user has named them (positions or a description).
38
+
39
+ ## Working method
40
+
41
+ 1. **Global correction first.** Exposure and white balance as in `pixerate-image-photo-correction`,
42
+ so retouching is judged on the final tones.
43
+ **Check:** skin `measure_region` has a plausible colour (red > green > blue, no strong cast).
44
+ 2. **Find the flaws.** `capture_view` the face at 2-4× scale in parts; list spots, hairs and
45
+ flaws with positions. `measure_region` on a small box around a suspect spot compared with a
46
+ box of clean skin beside it tells a spot from a shadow.
47
+ **Check:** a list of flaws with coordinates and sizes.
48
+ 3. **Make a retouch layer.** `add_pixel_layer` named "Retouch" above the photo; all healing goes
49
+ on it with `sample: "current-below"`.
50
+ **Check:** `list_layers` shows it active above the photo.
51
+ 4. **Heal small to large.** `spot_heal` for spots (one dab, `brush.size` a little larger than the
52
+ spot, `hardness` 0.2-0.5); a short stroke of dabs along a stray hair. `heal_brush` from a clean
53
+ source of the same texture for larger flaws; `clone_stamp` near edges (lips, eyes, hairline)
54
+ where healing would pull colour across.
55
+ **Check:** `measure_region` around each flaw: its `p1` is near the clean skin's; `capture_view`
56
+ shows texture, not a smear.
57
+ 5. **Even tone (dodge and burn)**, only if asked for more than a clean-up. Duplicate the photo
58
+ (`duplicate_layer`) and `dodge` / `burn` at `exposure` 0.1-0.2, `range: "midtones"`, along
59
+ shadows under the eyes or uneven patches; or paint on a layer in `soft-light` blend.
60
+ **Check:** the area's luminance moved by a few levels, not tens.
61
+ 6. **Eyes and teeth.** Select them (`select_ellipse`, `select_polygon`), feather 1-2 px, and add a
62
+ `levels` or `curves` layer while the selection is active (`master.gamma` 1.2-1.6). For yellow
63
+ teeth, a `hueSaturation` layer lowering the yellows' saturation, masked the same way.
64
+ **Check:** `measure_region` inside rises; just outside (skin, lips) does not change.
65
+ 7. **Review the whole face** at normal scale (`render_preview` or `capture_view` at 1×). Lower a
66
+ layer's `opacity` where it went too far (`set_layer_properties`). Report what you removed.
67
+
68
+ ## Decision table
69
+
70
+ | Flaw | Tool |
71
+ | --- | --- |
72
+ | Spot, pimple, dust (smaller than about 15 px) | `spot_heal`, `proximity-match` or `content-aware` |
73
+ | Stray hair across skin | `spot_heal` along it, short strokes; `clone_stamp` where it crosses an edge |
74
+ | Larger blemish, scar the user wants gone | `heal_brush` from clean skin of the same texture and light |
75
+ | Flaw next to an edge (lip line, eyelid, hairline) | `clone_stamp`, with a source along the same edge |
76
+ | Shine on forehead or nose | `heal_brush` at reduced brush opacity from matte skin nearby |
77
+ | Dark circles, uneven patches | `dodge` (midtones, exposure 0.1-0.2) on a duplicate, or a masked curves layer |
78
+ | Dull eyes | Masked `levels` gamma 1.2-1.6; `sharpen_stroke` lightly on the iris |
79
+ | Yellow teeth | Masked `hueSaturation` (yellows, saturation -20..-40), then a little brightness |
80
+ | Redness | Masked `hueSaturation` (reds, saturation down a little) |
81
+
82
+ ## Recipes
83
+
84
+ **Clean-up pass.** `add_pixel_layer` "Retouch" → one `spot_heal` per spot (`points: [{x, y}]`,
85
+ `sample: "current-below"`, `brush: { size, hardness: 0.3 }`) → `measure_region` around each.
86
+
87
+ **Stray hair.** `spot_heal` with `points` every 8-10 px along the hair and a brush 1.5-2× its
88
+ width; check the whole length with one `measure_region` box over it.
89
+
90
+ **Brighter eyes.** `select_ellipse` each eye (`mode: "add"` for the second) → `modify_selection`
91
+ `feather` 1 → `add_adjustment_layer` `levels` with `params: { master: { gamma: 1.4 } }` →
92
+ `measure_region` on an eye and on the skin beside it.
93
+
94
+ ## Pitfalls
95
+
96
+ - **Healing on the photo layer.** Mistakes become permanent; heal onto an empty layer with
97
+ `current-below`.
98
+ - **An empty layer with `sample: "current"`** has nothing to copy, so the stroke does nothing.
99
+ - **Over-smoothing.** Blurring skin removes pores and looks fake; heal flaws instead.
100
+ - **Big brushes.** A heal brush much larger than the flaw pulls in colour from around it; keep it
101
+ just over the flaw.
102
+ - **Healing across an edge** smears lips or lashes into skin; clone along edges instead.
103
+ - **Removing features.** Moles, freckles and scars are part of a face; ask before removing them.
104
+ - **Bright white eyes and teeth** look unnatural; small gamma moves, and measure.
105
+
106
+ ## Tools used
107
+
108
+ - `list_layers`, `capture_view`, `render_preview`, `measure_region`: find flaws, check each fix, review
109
+ - `add_pixel_layer`, `duplicate_layer`, `set_layer_properties`: retouch and dodge-and-burn layers; fade them
110
+ - `spot_heal`, `heal_brush`, `clone_stamp`, `set_clone_source`: remove spots, hairs and flaws
111
+ - `dodge`, `burn`, `sponge`: even tone and colour on a duplicate
112
+ - `sharpen_stroke`, `blur_stroke`: crisp eyes, soften a harsh edge, locally
113
+ - `select_ellipse`, `select_polygon`, `modify_selection`: select eyes and teeth
114
+ - `add_adjustment_layer`: masked brightening or colour change of eyes and teeth
115
+ - `undo_image`: take back a step
@@ -0,0 +1,25 @@
1
+ {
2
+ "prompt": "Brighten the eyes a little (the two dark ovals at x 35-45 and x 55-65, y 15-21) without changing the skin.",
3
+ "fixture": {
4
+ "editor": "image",
5
+ "width": 100,
6
+ "height": 60,
7
+ "background": [200, 160, 140],
8
+ "shapes": [
9
+ { "shape": "ellipse", "x": 35, "y": 15, "width": 10, "height": 6, "color": [70, 55, 50] },
10
+ { "shape": "ellipse", "x": 55, "y": 15, "width": 10, "height": 6, "color": [70, 55, 50] }
11
+ ]
12
+ },
13
+ "checks": [
14
+ { "why": "the eyes are brighter", "tool": "measure_region", "args": { "x": 38, "y": 17, "width": 4, "height": 2 }, "expect": { "stats": { "luminance": { "mean": { "$min": 75, "$max": 140 } } } } },
15
+ { "why": "the other eye too", "tool": "measure_region", "args": { "x": 58, "y": 17, "width": 4, "height": 2 }, "expect": { "stats": { "luminance": { "mean": { "$min": 75, "$max": 140 } } } } },
16
+ { "why": "the skin is unchanged", "tool": "measure_region", "args": { "x": 10, "y": 35, "width": 80, "height": 20 }, "expect": { "meanColor": "#c8a08c" } },
17
+ { "why": "it is an adjustable, masked layer", "tool": "list_layers", "expect": { "layers": { "$some": { "type": "adjustment", "mask": { "kind": "raster" } } } } }
18
+ ],
19
+ "solution": [
20
+ { "tool": "select_ellipse", "args": { "x": 35, "y": 15, "width": 10, "height": 6 } },
21
+ { "tool": "select_ellipse", "args": { "x": 55, "y": 15, "width": 10, "height": 6, "mode": "add" } },
22
+ { "tool": "modify_selection", "args": { "op": "feather", "radius": 1 } },
23
+ { "tool": "add_adjustment_layer", "args": { "kind": "levels", "name": "Eyes", "params": { "master": { "gamma": 1.6 } } } }
24
+ ]
25
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "prompt": "Remove the two dark blemishes on this skin (around x 30, y 30 and x 70, y 22). Don't change the original photo layer.",
3
+ "fixture": {
4
+ "editor": "image",
5
+ "width": 100,
6
+ "height": 60,
7
+ "background": [200, 160, 140],
8
+ "shapes": [
9
+ { "shape": "rect", "x": 29, "y": 29, "width": 3, "height": 3, "color": [120, 80, 70] },
10
+ { "shape": "rect", "x": 69, "y": 21, "width": 3, "height": 3, "color": [120, 80, 70] }
11
+ ]
12
+ },
13
+ "checks": [
14
+ { "why": "the first blemish is gone", "tool": "measure_region", "args": { "x": 26, "y": 26, "width": 9, "height": 9 }, "expect": { "stats": { "luminance": { "p1": { "$min": 155 } } } } },
15
+ { "why": "the second blemish is gone", "tool": "measure_region", "args": { "x": 66, "y": 18, "width": 9, "height": 9 }, "expect": { "stats": { "luminance": { "p1": { "$min": 155 } } } } },
16
+ { "why": "the retouching is on its own layer above the photo", "tool": "list_layers", "expect": { "layers": { "$length": { "$min": 2 }, "$some": { "id": "background", "mask": null } } } }
17
+ ],
18
+ "solution": [
19
+ { "tool": "add_pixel_layer", "args": { "name": "Retouch" } },
20
+ { "tool": "spot_heal", "args": { "points": [{ "x": 30, "y": 30 }], "sample": "current-below", "brush": { "size": 10, "hardness": 0.3 } } },
21
+ { "tool": "spot_heal", "args": { "points": [{ "x": 70, "y": 22 }], "sample": "current-below", "brush": { "size": 10, "hardness": 0.3 } } }
22
+ ]
23
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "prompt": "There's a stray dark hair across the cheek (a thin line at y 45, from x 20 to 60). Remove it without touching the original layer.",
3
+ "fixture": {
4
+ "editor": "image",
5
+ "width": 100,
6
+ "height": 60,
7
+ "background": [200, 160, 140],
8
+ "shapes": [{ "shape": "rect", "x": 20, "y": 45, "width": 41, "height": 1, "color": [90, 70, 60] }]
9
+ },
10
+ "checks": [
11
+ { "why": "the hair is gone along its whole length", "tool": "measure_region", "args": { "x": 18, "y": 43, "width": 45, "height": 5 }, "expect": { "stats": { "luminance": { "p1": { "$min": 155 } } } } },
12
+ { "why": "on its own layer", "tool": "list_layers", "expect": { "layers": { "$length": { "$min": 2 } } } }
13
+ ],
14
+ "solution": [
15
+ { "tool": "add_pixel_layer", "args": { "name": "Retouch" } },
16
+ { "tool": "spot_heal", "args": { "points": [{ "x": 20, "y": 45.5 }, { "x": 30, "y": 45.5 }, { "x": 40, "y": 45.5 }, { "x": 50, "y": 45.5 }, { "x": 60, "y": 45.5 }], "sample": "current-below", "brush": { "size": 8, "hardness": 0.3 } } }
17
+ ]
18
+ }
@@ -0,0 +1,126 @@
1
+ ---
2
+ name: pixerate-image-product-photo
3
+ description: The product photo workflow in the Pixerate image editor - cutting a product out of its backdrop, putting it on pure white or a brand colour, grounding it with a soft shadow, keeping its colours accurate, cropping and centring it to marketplace ratios with consistent margins, and checking the result before export. Use when asked to prepare product shots, packshots or e-commerce, marketplace, Amazon, Etsy or catalogue images, put a product on white, remove or replace a product background, add a product shadow, or crop and centre products consistently, or when select_subject, add_layer_mask, add_layer_effect or set_crop are available. Load with pixerate-image-editor, which lists every tool.
4
+ metadata:
5
+ kind: workflow
6
+ reference: pixerate-image-editor
7
+ ---
8
+
9
+ # Preparing a product photo
10
+
11
+ A product shot is a cutout on a controlled background: the product's edge and colour must be
12
+ exact, the background uniform, and every image in a set framed the same way. Build it as layers
13
+ so each part stays adjustable: the photo with a mask hiding its backdrop, a background fill
14
+ layer below it, a shadow (an effect, or its own layer) between, and the crop on top.
15
+
16
+ This skill is the method. `pixerate-image-editor` has the full tool reference, and
17
+ `pixerate-image-selection-masking` the detail of selecting and refining the edge.
18
+
19
+ ## Goal and quality bar
20
+
21
+ - The background is uniform: pure white is 255 in every channel (`measure_region` `stats`
22
+ luminance `p1` ≥ 250 and a cast within ±2), or exactly the brand colour asked for.
23
+ - The product's colour is unchanged by the cutout (measure it before and after) and its edge
24
+ has no halo of the old backdrop (`inspect_mask_edge` `haloScore` under 0.35).
25
+ - The product sits on something: a soft shadow below it, unless the user wants it floating.
26
+ - The frame matches the destination: ratio, product centred (or as the set requires), and the
27
+ product filling a consistent share of the frame (about 75-85 % of the longer side for most
28
+ marketplaces).
29
+ - Everything stays editable: masked, not erased; the shadow an effect or a layer.
30
+
31
+ ## Inputs to confirm
32
+
33
+ - **Background:** pure white (the default for marketplaces), a colour (hex), or transparent.
34
+ - **Destination ratio and margin:** 1:1 (most marketplaces), 4:5, 3:2, or a size in pixels; how
35
+ much of the frame the product should fill. For a set, the same for every image.
36
+ - **Shadow:** a soft contact or drop shadow (the default), a reflection, or none.
37
+ - **Colour reference:** a swatch or hex the product must match, when colour accuracy matters.
38
+
39
+ ## Working method
40
+
41
+ 1. **Read and measure the product.** `list_layers`, `get_image_info`, then `measure_region` on a
42
+ flat part of the product: its mean colour is the reference you will protect.
43
+ **Check:** you have the product's bounds and colour.
44
+ 2. **Correct the photo first** if it needs it (white balance from the backdrop, exposure): the
45
+ cutout and the measure should be on corrected colours (`pixerate-image-photo-correction`).
46
+ **Check:** the backdrop is neutral before you remove it.
47
+ 3. **Select the product.** On a plain backdrop, `magic_wand` on the product (`contiguous: true`)
48
+ or on the backdrop and `invert_selection`; on a busy one, `select_subject` or `select_object`
49
+ with a box, then `refine_selection`. Hard-edged products want a crisp edge (refine radius
50
+ 1-3, no feather).
51
+ **Check:** `get_selection` bounds hug the product; `inspect_mask_edge` `issues` is empty.
52
+ 4. **Hide the backdrop, do not delete it.** `add_layer_mask` `from: "selection"` on the photo
53
+ layer (or `add_vector_mask` for a geometric product that must stay razor sharp). Then
54
+ `add_pixel_layer` with the background `color` placed `belowId` the photo layer.
55
+ **Check:** `measure_region` on a corner: `p1` ≥ 250 (white) or the brand colour; on the
56
+ product: the colour from step 1.
57
+ 5. **Fix the edge colour** if the old backdrop tints it: `set_edge_decontamination`, or
58
+ `modify_selection` `contract` 1 px before masking.
59
+ **Check:** `inspect_mask_edge` with `target: "layer-mask"` shows no halo.
60
+ 6. **Ground it.** With the product on its own layer (`layer_via_copy`, or the masked photo),
61
+ `add_layer_effect` `drop-shadow` with `angle` 90 (light from above), a small `distance`
62
+ (3-10 px at web sizes), a `size` larger than the distance and `opacity` 0.25-0.45.
63
+ **Check:** `list_layers` shows the effect; the shadow reads as contact, not as a cutout.
64
+ 7. **Frame it.** `set_crop` at the ratio, centred on the product's bounds, with the margin asked.
65
+ **Check:** `get_image_info` `crop`: ratio right, centre within a pixel or two of the
66
+ product's centre, product share of the side as intended.
67
+ 8. **Final check.** `measure_region` on the background corners and the product; `render_preview`
68
+ where available. Report the crop, the background colour and the shadow settings so the rest
69
+ of the set can match.
70
+
71
+ ## Decision table
72
+
73
+ | Backdrop | Selection |
74
+ | --- | --- |
75
+ | Plain, contrasting | `magic_wand` on the product or backdrop (tolerance 10-30) |
76
+ | Plain but close to the product colour | `select_color_range` on the backdrop with low fuzziness, then refine |
77
+ | Busy or textured | `select_subject`, or `select_object` with the product's box |
78
+ | Glass, transparent or reflective product | Select the outline, then lower the mask's `density` where it should show through; ask how it should look |
79
+ | Product with straight edges (a box, a screen) | `add_vector_mask` from the selection, or a path |
80
+
81
+ | Destination | Frame |
82
+ | --- | --- |
83
+ | Marketplace main image | 1:1, white, product about 85 % of the frame |
84
+ | Catalogue or site grid | The grid's ratio, the same margin on every image |
85
+ | Social post | 4:5 or 1:1, room for text if the user asks |
86
+
87
+ ## Recipes
88
+
89
+ **On white.** `magic_wand` on the product → `inspect_mask_edge` → `add_layer_mask` (`layerId`
90
+ of the photo, `from: "selection"`) → `add_pixel_layer` (`color: "#ffffff"`, `belowId` the photo)
91
+ → `measure_region` on a corner and on the product.
92
+
93
+ **Centred square crop.** Product bounds `x, y, w, h`; side `S = max(w, h) / share` (share 0.7-0.85,
94
+ no larger than the image allows); `set_crop` with `x + w/2 - S/2`, `y + h/2 - S/2`, `S`, `S`.
95
+
96
+ **Contact shadow.** `layer_via_copy` the selected product (or use the masked layer), then
97
+ `add_layer_effect` `kind: "drop-shadow"` with `settings: { angle: 90, distance: 6, size: 10,
98
+ opacity: 0.4 }`; adjust with `update_layer_effect`.
99
+
100
+ ## Pitfalls
101
+
102
+ - **Off-white that looks white.** 245 grey reads as white on screen and fails a marketplace
103
+ check; measure `p1`, not the mean.
104
+ - **Erasing the backdrop.** Deleted pixels cannot be fixed later; mask instead.
105
+ - **Colour shift from the cutout.** A decontamination or a colour adjustment above the product
106
+ changes it; measure the product before and after, and keep corrections below the product.
107
+ - **A halo of the old backdrop** shows on white more than anywhere; inspect the edge and
108
+ decontaminate or contract by a pixel.
109
+ - **A shadow that floats.** A large distance or a sideways angle looks pasted on; keep it short
110
+ and below.
111
+ - **Inconsistent sets.** Each image cropped by eye drifts; compute the crop from the product's
112
+ bounds with the same share for every image.
113
+
114
+ ## Tools used
115
+
116
+ - `list_layers`, `get_image_info`, `measure_region`: read the stack, the frame and the colours
117
+ - `magic_wand`, `select_color_range`, `select_subject`, `select_object`, `invert_selection`: select the product
118
+ - `refine_selection`, `modify_selection`, `get_selection`, `inspect_mask_edge`: refine and check the edge
119
+ - `add_layer_mask`, `add_vector_mask`, `set_layer_mask_properties`: hide the backdrop, editably
120
+ - `set_edge_decontamination`: remove the backdrop's colour from the edge
121
+ - `add_pixel_layer`: the background fill below the product
122
+ - `layer_via_copy`: the product on its own layer
123
+ - `add_layer_effect`, `update_layer_effect`: the shadow
124
+ - `set_crop`, `reset_crop`: the frame
125
+ - `render_preview`: look at the result
126
+ - `undo_image`: take back a step
@@ -0,0 +1,19 @@
1
+ {
2
+ "prompt": "The product looks like it's floating. Put it on its own layer and add a soft shadow under it that I can still adjust.",
3
+ "fixture": {
4
+ "editor": "image",
5
+ "width": 120,
6
+ "height": 90,
7
+ "background": [200, 198, 195],
8
+ "shapes": [{ "shape": "rect", "x": 40, "y": 25, "width": 40, "height": 40, "color": [30, 90, 160] }]
9
+ },
10
+ "checks": [
11
+ { "why": "the product is on its own layer, cut to its shape", "tool": "list_layers", "expect": { "layers": { "$some": { "type": "pixel", "bounds": { "x": { "$min": 39, "$max": 41 }, "y": { "$min": 24, "$max": 26 }, "width": { "$min": 39, "$max": 42 }, "height": { "$min": 39, "$max": 42 } } } } } },
12
+ { "why": "a live drop shadow on that layer", "tool": "list_layers", "expect": { "layers": { "$some": { "effects": { "$expr": "v && JSON.stringify(v).includes('drop-shadow')" } } } } }
13
+ ],
14
+ "solution": [
15
+ { "tool": "magic_wand", "args": { "x": 60, "y": 45, "tolerance": 20, "contiguous": true } },
16
+ { "tool": "layer_via_copy", "args": {} },
17
+ { "tool": "add_layer_effect", "args": { "layerId": { "$read": "list_layers", "path": "activeLayerId" }, "kind": "drop-shadow", "settings": { "distance": 6, "size": 10, "opacity": 0.4, "angle": 90 } } }
18
+ ]
19
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "prompt": "Crop to a square for the marketplace listing with the product centred, leaving some breathing room around it (the product should fill roughly two thirds of the frame).",
3
+ "fixture": {
4
+ "editor": "image",
5
+ "width": 120,
6
+ "height": 90,
7
+ "background": [200, 198, 195],
8
+ "shapes": [{ "shape": "rect", "x": 40, "y": 25, "width": 40, "height": 40, "color": [30, 90, 160] }]
9
+ },
10
+ "checks": [
11
+ { "why": "a square crop", "tool": "get_image_info", "expect": { "crop": { "$expr": "v && Math.abs(v.width - v.height) <= 1" } } },
12
+ { "why": "the product is centred (within 2 px)", "tool": "get_image_info", "expect": { "crop": { "$expr": "v && Math.abs(v.x + v.width / 2 - 60) <= 2 && Math.abs(v.y + v.height / 2 - 45) <= 2" } } },
13
+ { "why": "the product fills 60-85 % of the side", "tool": "get_image_info", "expect": { "crop": { "$expr": "v && 40 / v.width >= 0.6 && 40 / v.width <= 0.85" } } }
14
+ ],
15
+ "solution": [
16
+ { "tool": "get_image_info" },
17
+ { "tool": "set_crop", "args": { "x": 32, "y": 17, "width": 56, "height": 56 } }
18
+ ]
19
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "prompt": "Put this product on a pure white background for the shop, without changing the product's colour. Keep it editable.",
3
+ "fixture": {
4
+ "editor": "image",
5
+ "width": 120,
6
+ "height": 90,
7
+ "background": [200, 198, 195],
8
+ "shapes": [{ "shape": "rect", "x": 40, "y": 25, "width": 40, "height": 40, "color": [30, 90, 160] }]
9
+ },
10
+ "checks": [
11
+ { "why": "the background is pure, neutral white", "tool": "measure_region", "args": { "x": 0, "y": 0, "width": 30, "height": 20 }, "expect": { "stats": { "luminance": { "p1": { "$min": 250 } } }, "cast": { "redBlue": { "$min": -2, "$max": 2 } } } },
12
+ { "why": "the area under the product is white too", "tool": "measure_region", "args": { "x": 0, "y": 72, "width": 120, "height": 18 }, "expect": { "stats": { "luminance": { "p1": { "$min": 250 } } } } },
13
+ { "why": "the product keeps its colour", "tool": "measure_region", "args": { "x": 45, "y": 30, "width": 30, "height": 30 }, "expect": { "stats": { "red": { "mean": { "$min": 26, "$max": 34 } }, "green": { "mean": { "$min": 86, "$max": 94 } }, "blue": { "mean": { "$min": 156, "$max": 164 } } } } },
14
+ { "why": "the backdrop is hidden by a mask, not deleted", "tool": "list_layers", "expect": { "layers": { "$some": { "type": "pixel", "mask": { "kind": "raster" } } } } }
15
+ ],
16
+ "solution": [
17
+ { "tool": "magic_wand", "args": { "x": 60, "y": 45, "tolerance": 20, "contiguous": true } },
18
+ { "tool": "inspect_mask_edge" },
19
+ { "tool": "add_layer_mask", "args": { "layerId": "background", "from": "selection" } },
20
+ { "tool": "add_pixel_layer", "args": { "name": "White", "color": "#ffffff", "belowId": "background" } },
21
+ { "tool": "measure_region", "args": { "x": 0, "y": 0, "width": 30, "height": 20 } }
22
+ ]
23
+ }