@pixerate/editor 0.221.0 → 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.
Files changed (34) hide show
  1. package/dist/image-editor/edit-tools.d.cts +1 -1
  2. package/dist/image-editor/edit-tools.d.ts +1 -1
  3. package/dist/image-editor/index.cjs +52 -4
  4. package/dist/image-editor/index.js +52 -4
  5. package/dist/image-editor/selection/inspect.d.cts +1 -1
  6. package/dist/image-editor/selection/inspect.d.ts +1 -1
  7. package/package.json +1 -1
  8. package/skills/README.md +43 -9
  9. package/skills/WORKFLOW_TEMPLATE.md +50 -0
  10. package/skills/pixerate-image-editor/SKILL.md +32 -76
  11. package/skills/pixerate-image-editor/references/free-transform.md +11 -0
  12. package/skills/pixerate-image-editor/references/layer-effects.md +12 -0
  13. package/skills/pixerate-image-editor/references/smart-objects.md +9 -0
  14. package/skills/pixerate-image-photo-correction/SKILL.md +129 -0
  15. package/skills/pixerate-image-photo-correction/evals/crop-for-instagram.json +19 -0
  16. package/skills/pixerate-image-photo-correction/evals/lift-shadows-keep-sky.json +20 -0
  17. package/skills/pixerate-image-photo-correction/evals/neutralise-cast.json +25 -0
  18. package/skills/pixerate-image-portrait-retouch/SKILL.md +115 -0
  19. package/skills/pixerate-image-portrait-retouch/evals/brighten-eyes.json +25 -0
  20. package/skills/pixerate-image-portrait-retouch/evals/remove-blemishes.json +23 -0
  21. package/skills/pixerate-image-portrait-retouch/evals/remove-stray-hair.json +18 -0
  22. package/skills/pixerate-image-product-photo/SKILL.md +126 -0
  23. package/skills/pixerate-image-product-photo/evals/grounding-shadow.json +19 -0
  24. package/skills/pixerate-image-product-photo/evals/square-crop.json +19 -0
  25. package/skills/pixerate-image-product-photo/evals/white-background.json +23 -0
  26. package/skills/pixerate-image-selection-masking/evals/local-adjustment.json +22 -0
  27. package/skills/pixerate-image-selection-masking/evals/mask-out-background.json +24 -0
  28. package/skills/pixerate-image-selection-masking/evals/select-one-of-two.json +22 -0
  29. package/skills/pixerate-spreadsheet/SKILL.md +7 -0
  30. package/skills/pixerate-spreadsheet-clean-import/SKILL.md +154 -0
  31. package/skills/pixerate-spreadsheet-clean-import/evals/clean-in-place.json +43 -0
  32. package/skills/pixerate-spreadsheet-clean-import/evals/dedupe-keep-latest.json +25 -0
  33. package/skills/pixerate-spreadsheet-clean-import/evals/fix-types-only.json +24 -0
  34. package/skills/pixerate-video-editor/SKILL.md +8 -0
@@ -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
  */
@@ -2807,6 +2807,21 @@ function inspectMaskEdge(mask, image, options = {}) {
2807
2807
  blk.x1 = Math.max(blk.x1, x);
2808
2808
  blk.y1 = Math.max(blk.y1, y);
2809
2809
  };
2810
+ const antiAliased = (x, y) => {
2811
+ let full = false;
2812
+ let none = false;
2813
+ for (let dy = -2; dy <= 2; dy++) {
2814
+ for (let dx = -2; dx <= 2; dx++) {
2815
+ const nx = x + dx;
2816
+ const ny = y + dy;
2817
+ if (nx < 0 || ny < 0 || nx >= W || ny >= H) continue;
2818
+ const n = a[ny * W + nx];
2819
+ if (n === 255) full = true;
2820
+ else if (n === 0) none = true;
2821
+ }
2822
+ }
2823
+ return full && none;
2824
+ };
2810
2825
  const x0 = Math.max(0, b.x - 1);
2811
2826
  const y0 = Math.max(0, b.y - 1);
2812
2827
  const x1 = Math.min(W, b.x + b.width + 1);
@@ -2836,7 +2851,7 @@ function inspectMaskEdge(mask, image, options = {}) {
2836
2851
  const partial = v > 0 && v < 255;
2837
2852
  if (hi && lo || partial) {
2838
2853
  band++;
2839
- if (partial) soft++;
2854
+ if (partial && !antiAliased(x, y)) soft++;
2840
2855
  }
2841
2856
  if (v < 128) continue;
2842
2857
  const inCanvasLow = (nx, ny) => nx >= 0 && ny >= 0 && nx < W && ny < H && a[ny * W + nx] < 128;
@@ -13784,7 +13799,7 @@ function createImageSelectionTools(controller) {
13784
13799
  const inspectEdge = {
13785
13800
  name: "inspect_mask_edge",
13786
13801
  title: "Inspect a mask edge",
13787
- description: "Measures the edge of the selection (default), a layer's painted mask (layer-mask) or its vector mask (vector-mask), so you can check it without looking. Returns haloScore 0-1 (background colour left just inside the edge; worse above 0.35), edgeAlignment 0-1 (share of the edge on strong image edges; worse below 0.5), jaggedness (outline turning in radians per 100 px beyond what closing it needs; smooth outlines score under 3, worse above 15), softFraction 0-1 (share of edge pixels partly selected), holes and islands (count, total area, the 10 largest with bounds), worst (up to 5 regions with a reason, ready for capture_view rect) and issues (findings with a suggested fix; empty when clean). Measures a pixel layer's masks against its own pixels, otherwise the composite. Read-only: inspect, fix the issues, inspect again.",
13802
+ description: "Measures the edge of the selection (default), a layer's painted mask (layer-mask) or its vector mask (vector-mask), so you can check it without looking. Returns haloScore 0-1 (background colour left just inside the edge; worse above 0.35), edgeAlignment 0-1 (share of the edge on strong image edges; worse below 0.5), jaggedness (outline turning in radians per 100 px beyond what closing it needs; smooth outlines score under 3, worse above 15), softFraction 0-1 (share of edge pixels in a transition wider than anti-aliasing), holes and islands (count, total area, the 10 largest with bounds), worst (up to 5 regions with a reason, ready for capture_view rect) and issues (findings with a suggested fix; empty when clean). Measures a pixel layer's masks against its own pixels, otherwise the composite. Read-only: inspect, fix the issues, inspect again.",
13788
13803
  inputSchema: {
13789
13804
  type: "object",
13790
13805
  properties: {
@@ -15310,10 +15325,43 @@ function createImageEditTools(controller) {
15310
15325
  annotations: { title: "Read the histogram", ...READ },
15311
15326
  handler: ({ bins }) => {
15312
15327
  const h = controller.getHistogram();
15313
- 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.");
15314
15329
  return histogramSummary(h, bins);
15315
15330
  }
15316
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
+ };
15317
15365
  const updateLayer = {
15318
15366
  name: "update_adjustment_layer",
15319
15367
  title: "Change an adjustment layer",
@@ -15470,7 +15518,7 @@ function createImageEditTools(controller) {
15470
15518
  return history();
15471
15519
  }
15472
15520
  };
15473
- 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];
15474
15522
  }
15475
15523
 
15476
15524
  // src/image-editor/layer-tools.ts
@@ -2807,6 +2807,21 @@ function inspectMaskEdge(mask, image, options = {}) {
2807
2807
  blk.x1 = Math.max(blk.x1, x);
2808
2808
  blk.y1 = Math.max(blk.y1, y);
2809
2809
  };
2810
+ const antiAliased = (x, y) => {
2811
+ let full = false;
2812
+ let none = false;
2813
+ for (let dy = -2; dy <= 2; dy++) {
2814
+ for (let dx = -2; dx <= 2; dx++) {
2815
+ const nx = x + dx;
2816
+ const ny = y + dy;
2817
+ if (nx < 0 || ny < 0 || nx >= W || ny >= H) continue;
2818
+ const n = a[ny * W + nx];
2819
+ if (n === 255) full = true;
2820
+ else if (n === 0) none = true;
2821
+ }
2822
+ }
2823
+ return full && none;
2824
+ };
2810
2825
  const x0 = Math.max(0, b.x - 1);
2811
2826
  const y0 = Math.max(0, b.y - 1);
2812
2827
  const x1 = Math.min(W, b.x + b.width + 1);
@@ -2836,7 +2851,7 @@ function inspectMaskEdge(mask, image, options = {}) {
2836
2851
  const partial = v > 0 && v < 255;
2837
2852
  if (hi && lo || partial) {
2838
2853
  band++;
2839
- if (partial) soft++;
2854
+ if (partial && !antiAliased(x, y)) soft++;
2840
2855
  }
2841
2856
  if (v < 128) continue;
2842
2857
  const inCanvasLow = (nx, ny) => nx >= 0 && ny >= 0 && nx < W && ny < H && a[ny * W + nx] < 128;
@@ -13784,7 +13799,7 @@ function createImageSelectionTools(controller) {
13784
13799
  const inspectEdge = {
13785
13800
  name: "inspect_mask_edge",
13786
13801
  title: "Inspect a mask edge",
13787
- description: "Measures the edge of the selection (default), a layer's painted mask (layer-mask) or its vector mask (vector-mask), so you can check it without looking. Returns haloScore 0-1 (background colour left just inside the edge; worse above 0.35), edgeAlignment 0-1 (share of the edge on strong image edges; worse below 0.5), jaggedness (outline turning in radians per 100 px beyond what closing it needs; smooth outlines score under 3, worse above 15), softFraction 0-1 (share of edge pixels partly selected), holes and islands (count, total area, the 10 largest with bounds), worst (up to 5 regions with a reason, ready for capture_view rect) and issues (findings with a suggested fix; empty when clean). Measures a pixel layer's masks against its own pixels, otherwise the composite. Read-only: inspect, fix the issues, inspect again.",
13802
+ description: "Measures the edge of the selection (default), a layer's painted mask (layer-mask) or its vector mask (vector-mask), so you can check it without looking. Returns haloScore 0-1 (background colour left just inside the edge; worse above 0.35), edgeAlignment 0-1 (share of the edge on strong image edges; worse below 0.5), jaggedness (outline turning in radians per 100 px beyond what closing it needs; smooth outlines score under 3, worse above 15), softFraction 0-1 (share of edge pixels in a transition wider than anti-aliasing), holes and islands (count, total area, the 10 largest with bounds), worst (up to 5 regions with a reason, ready for capture_view rect) and issues (findings with a suggested fix; empty when clean). Measures a pixel layer's masks against its own pixels, otherwise the composite. Read-only: inspect, fix the issues, inspect again.",
13788
13803
  inputSchema: {
13789
13804
  type: "object",
13790
13805
  properties: {
@@ -15310,10 +15325,43 @@ function createImageEditTools(controller) {
15310
15325
  annotations: { title: "Read the histogram", ...READ },
15311
15326
  handler: ({ bins }) => {
15312
15327
  const h = controller.getHistogram();
15313
- 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.");
15314
15329
  return histogramSummary(h, bins);
15315
15330
  }
15316
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
+ };
15317
15365
  const updateLayer = {
15318
15366
  name: "update_adjustment_layer",
15319
15367
  title: "Change an adjustment layer",
@@ -15470,7 +15518,7 @@ function createImageEditTools(controller) {
15470
15518
  return history();
15471
15519
  }
15472
15520
  };
15473
- 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];
15474
15522
  }
15475
15523
 
15476
15524
  // src/image-editor/layer-tools.ts
@@ -31,7 +31,7 @@ export interface MaskEdgeReport {
31
31
  edgeAlignment: number;
32
32
  /** Turning of the outline beyond what closing it needs, in radians per 100 px of outline (0: smooth). */
33
33
  jaggedness: number;
34
- /** 0–1: the share of edge pixels that are partly covered (0 < α < 1). */
34
+ /** 0–1: the share of edge pixels in a transition wider than anti-aliasing (partly covered, no fully selected and unselected pixel within 2 px). */
35
35
  softFraction: number;
36
36
  /** Unselected areas enclosed by the selection, largest first (at most 10 listed). */
37
37
  holes: {
@@ -31,7 +31,7 @@ export interface MaskEdgeReport {
31
31
  edgeAlignment: number;
32
32
  /** Turning of the outline beyond what closing it needs, in radians per 100 px of outline (0: smooth). */
33
33
  jaggedness: number;
34
- /** 0–1: the share of edge pixels that are partly covered (0 < α < 1). */
34
+ /** 0–1: the share of edge pixels in a transition wider than anti-aliasing (partly covered, no fully selected and unselected pixel within 2 px). */
35
35
  softFraction: number;
36
36
  /** Unselected areas enclosed by the selection, largest first (at most 10 listed). */
37
37
  holes: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixerate/editor",
3
- "version": "0.221.0",
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
@@ -5,6 +5,11 @@ Each editor domain in `@pixerate/editor` ships an [Agent Skill](https://agentski
5
5
  expert in the domain would follow. They are published with the package, so an app or agent
6
6
  that uses the tools can load the matching skill.
7
7
 
8
+ ## Reference skills
9
+
10
+ One per editor domain. A reference skill owns its domain's tool and command tables, its mental
11
+ model and its general practice.
12
+
8
13
  | Skill | Covers |
9
14
  | --- | --- |
10
15
  | [`pixerate-agent-tools`](pixerate-agent-tools/SKILL.md) | What every editor has in common: the MCP bridge, toolsets and tool naming, finding tools with `list_toolsets` / `search_tools` / `enable_tools` / `get_skill`, `list_commands` / `run_command` / `run_commands`, errors, undo. Load it with any domain skill. |
@@ -15,12 +20,9 @@ that uses the tools can load the matching skill.
15
20
  | [`pixerate-chart`](pixerate-chart/SKILL.md) | Chart specs, data, series, presets, trendlines, Office and Mermaid charts. |
16
21
  | [`pixerate-diagram`](pixerate-diagram/SKILL.md) | Mermaid diagrams: nodes, edges, groups, layout. |
17
22
  | [`pixerate-image-editor`](pixerate-image-editor/SKILL.md) | Adjustments, adjustment layers, selections and masks. |
18
- | [`pixerate-image-selection-masking`](pixerate-image-selection-masking/SKILL.md) | Workflow: cutouts, background and sky replacement, local adjustments through masks; choosing a selection method, checking and refining edges, and outputting an editable mask. Built on `pixerate-image-editor`'s tools. |
19
23
  | [`pixerate-paint-editor`](pixerate-paint-editor/SKILL.md) | Natural-media painting: paper and grain, dry-media brushes, the brush library, strokes as data, pigment mixing. |
20
24
  | [`pixerate-animation-editor`](pixerate-animation-editor/SKILL.md) | Vector animation: shots, commands, timing, Flair and Lottie. |
21
25
  | [`pixerate-video-editor`](pixerate-video-editor/SKILL.md) | Non-linear video editing: tracks, clips, transitions, export. |
22
- | [`pixerate-talking-head-edit`](pixerate-talking-head-edit/SKILL.md) | Talking-head cleanup by transcript: fillers, long pauses and stutters found in one pass and cut as one plan reviewed before it applies. |
23
- | [`pixerate-bgm-alignment`](pixerate-bgm-alignment/SKILL.md) | Background music: beats and cuts on the beat, ducking under speech, loudness in LUFS. |
24
26
  | [`pixerate-generation`](pixerate-generation/SKILL.md) | Planning, estimating and running paid media generation. |
25
27
  | [`pixerate-parameters`](pixerate-parameters/SKILL.md) | Effect parameters, presets and keyframes. |
26
28
  | [`pixerate-tokens`](pixerate-tokens/SKILL.md) | Design tokens: collections, modes, variables and aliases, through the whole-set document tools. |
@@ -28,6 +30,23 @@ that uses the tools can load the matching skill.
28
30
  | [`pixerate-color`](pixerate-color/SKILL.md) | Colour inspection, WCAG accessibility and contrast optimization, CVD simulation, palette audits, harmonies, and OKLab/OKLCH blends. |
29
31
  | [`pixerate-typography`](pixerate-typography/SKILL.md) | Fonts and fallback stacks, font loading, modular type scales and hierarchies, fluid sizes, readability and type-aware contrast audits, text segmentation, the font picker. |
30
32
 
33
+ ## Workflow skills
34
+
35
+ A workflow skill covers one job from start to finish: what to confirm first, the steps in
36
+ order with a check after each, a decision table, recipes and the job's pitfalls. It is built on
37
+ its reference skill's tools (load both), and its description is written about the job, so a
38
+ request like "clean up this export" finds it.
39
+
40
+ | Skill | Editor | Covers |
41
+ | --- | --- | --- |
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. |
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. |
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. |
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. |
49
+
31
50
  ## Using them
32
51
 
33
52
  Copy or symlink the directories into your agent's skills folder, for example for Claude Code:
@@ -75,12 +94,27 @@ same change.
75
94
  `create*ToolProvider` / `create*Commands` factory is not mapped to a skill. A new domain
76
95
  gets a new skill directory, an entry in `expectedBySkill()` and `FACTORIES`, and a row in
77
96
  the table above.
78
- - **Workflow skills.** A skill that covers one job end to end on another skill's tools (for
79
- example `pixerate-image-selection-masking` on `pixerate-image-editor`) has no tool table.
80
- Its frontmatter adds `metadata` with `kind: workflow` and `reference: <skill>`, it lists the
81
- tools it uses as bullets under `## Tools used`, and it is mapped in `WORKFLOWS` in the test
82
- (and to `[]` in `expectedBySkill()`). The test checks that every tool it uses exists in the
83
- reference skill.
97
+ - **Workflow skills.** A skill that covers one job end to end on another skill's tools has no
98
+ tool table (unless the job has its own provider, as the two video ones do). Start from
99
+ [`WORKFLOW_TEMPLATE.md`](WORKFLOW_TEMPLATE.md). Its frontmatter adds `metadata` with
100
+ `kind: workflow` and `reference: <skill>`; it lists the tools it uses as bullets under
101
+ `## Tools used`; and it is mapped in `WORKFLOWS` in the test (and to `[]` in
102
+ `expectedBySkill()`). The test checks that every tool it uses exists in the reference skill,
103
+ that every backticked tool or command name in its prose is real, that the reference skill
104
+ names it under `## Workflow skills`, that it stays under 250 body lines (aim for 200) and that
105
+ it ships evals. Add its row to the workflow table above.
106
+ - **Evals.** Each workflow skill ships at least three scenarios in `evals/*.json`, run by
107
+ `packages/core/tests/agent-skill-evals.test.ts`. An eval has a `prompt` (the task, in the
108
+ user's words), a `fixture` (the starting document: `{ "editor": "spreadsheet", "rows" }` or
109
+ `{ "editor": "image", "width", "height", "background", "shapes" }`), `checks` (a read-only
110
+ tool, its `args`, what its result must match in `expect`, and `why`) and a `solution` (the
111
+ tool calls an expert would make). The test proves each eval: some check fails on the fixture,
112
+ and all pass after the solution. A model harness runs the same file with the model in place
113
+ of the solution. In `expect`, objects match key by key and arrays element by element, with
114
+ operators for ranges and lists (`$min`, `$max`, `$match`, `$every`, `$some`, `$length`,
115
+ `$unique`, `$distinct`, `$sum`, `$column`, `$nonEmpty`, `$not`, `$in`, `$type`); a check's
116
+ argument can read a value the job created with `{ "$read": "list_layers", "path":
117
+ "layers.-1.id" }`. Checks test the outcome the user asked for, not one way of getting it.
84
118
  - **Workflows and practice.** The test cannot check advice. When a feature changes the best
85
119
  way to do something (a new batch tool, a new check, a better default), update the working
86
120
  method and best practices too.
@@ -0,0 +1,50 @@
1
+ ---
2
+ name: pixerate-<editor>-<job>
3
+ description: The <job> workflow in the Pixerate <editor> - <what it produces, in the user's words, and the main steps>. Use when asked to <trigger phrases: the verbs and nouns people use for this job>, or when <a tool or two that signal the job> are available. Load with pixerate-<editor>, which lists every tool.
4
+ metadata:
5
+ kind: workflow
6
+ reference: pixerate-<editor>
7
+ ---
8
+
9
+ # <The job, as a short title>
10
+
11
+ <Two to four sentences: the model of the job a person needs before starting. What the
12
+ document is made of here, and the one or two facts that make this job go right or wrong.>
13
+
14
+ This skill is the method. `pixerate-<editor>` has the full tool reference.
15
+
16
+ ## Goal and quality bar
17
+
18
+ - <What the finished result is, stated so it can be checked: numbers, ranges, absences.>
19
+ - <What must stay editable, reversible or untouched.>
20
+
21
+ ## Inputs to confirm
22
+
23
+ Ask when you cannot tell from the document; say what you assumed when you can.
24
+
25
+ - **<Decision>:** <the options, and the default when the user does not say>.
26
+
27
+ ## Working method
28
+
29
+ 1. **<Step>.** <What to do, with which tool and which arguments matter (units, defaults).>
30
+ **Check:** <a read tool and what its result must show before going on>.
31
+ 2. **<Step>.** ...
32
+ **Check:** ...
33
+
34
+ ## Decision table
35
+
36
+ | Situation | Approach |
37
+ | --- | --- |
38
+ | <case> | <method, tool, setting> |
39
+
40
+ ## Recipes
41
+
42
+ **<Common request>.** <The tool calls in order, with arguments, as short prose.>
43
+
44
+ ## Pitfalls
45
+
46
+ - **<Failure>.** <Why it happens and how to avoid or detect it.>
47
+
48
+ ## Tools used
49
+
50
+ - `<tool_name>`, `<tool_name>`: <one-line purpose in this job>
@@ -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).
@@ -33,25 +26,31 @@ Also an optional selection and crop/rotate/flip. See `pixerate-agent-tools` for
33
26
  - **Coordinates** for selections are natural pixels of the source image (before crop, rotate
34
27
  or flip). Read `get_selection` bounds or the image size, never screen pixels.
35
28
 
29
+ ## Workflow skills
30
+
31
+ For these jobs, load the workflow skill too: it has the method, the checks between steps and
32
+ the pitfalls, and uses the tools below.
33
+
34
+ - `pixerate-image-photo-correction`: global correction in order (frame, light, colour, local, look), with clipping and neutral checks.
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.
38
+
36
39
  ## Working method
37
40
 
38
41
  1. **Read.** `get_adjustments` for the current values, process version and layers;
39
42
  `get_image_info` for the image size, crop and rotation; `list_controls` once for control
40
43
  ids, ranges, layer kinds and blend modes; `render_preview` (default 512 px) to see the
41
44
  image, then `get_histogram` to judge exposure and clipping in numbers.
42
- 2. **Plan the order of corrections** (see Best practices): straighten and crop first (so
43
- later judgements are on the final frame), then global tone and white balance,
44
- then contrast and tone curves, then colour, then local fixes through masks, then creative
45
- looks.
45
+ 2. **Plan the order:** frame, light, colour, local, look (`pixerate-image-photo-correction`).
46
46
  3. **Write in few steps.** Put every base change of one correction into one `set_adjustments`
47
47
  call (one undo step) with a `label`. Create layers with their `params` in the same
48
48
  `add_adjustment_layer` call rather than adding a neutral layer and then setting it.
49
49
  4. **Local edits:** select, refine, check with `get_selection`, then `apply_selection_as_mask`
50
50
  with a `kind`. Load `pixerate-image-selection-masking` for cutouts, masks and local edits.
51
- 5. **Verify.** `render_preview` after each group of corrections, not after each slider, then
52
- `get_histogram` (it reads the last render) for clipping. Compare against the previous
53
- preview; read the `applied` and `clamped` lists that writes return. `undo_image` takes back a
54
- 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.
55
54
 
56
55
  ### Working with layers
57
56
 
@@ -85,12 +84,9 @@ filters, so nothing degrades. Read [references/smart-objects.md](references/smar
85
84
 
86
85
  ### Shapes and text
87
86
 
88
- Draw boxes, badges, arrows and callouts as shape layers (`add_shape_layer` with a live `shape`,
89
- then `set_shape_component` / `update_shape` / `add_shape_component`), and text as type layers
90
- (`add_text_layer`, then `set_text`, `set_text_style`, `set_paragraph_style` by `start` / `end`
91
- range). Both stay sharp through transforms and take effects, masks and blend modes; rasterize only
92
- to paint on them. Read [references/shapes-and-type.md](references/shapes-and-type.md) before
93
- 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.
94
90
 
95
91
  ### Layer effects
96
92
 
@@ -110,12 +106,11 @@ selection or locked transparency. Read [references/painting.md](references/paint
110
106
  ### Colour management
111
107
 
112
108
  Working space, profiles, soft proofing and print conversions: read [references/color-management.md](references/color-management.md).
109
+
113
110
  ### Retouching
114
111
 
115
- Clone Stamp, Healing Brush, Spot Healing, Smudge, Dodge, Burn, Sponge, Blur, Sharpen and the
116
- History Brush: retouch on an empty layer with `sample: "current-below"`, pick `spot_heal` for small
117
- spots, `heal_brush` for larger flaws and `clone_stamp` to copy exactly (`set_clone_transform` scales,
118
- 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.
119
114
 
120
115
  ### Photoshop files
121
116
 
@@ -128,27 +123,8 @@ in the source frame at the document's bit depth, colour mode and profile. Read
128
123
  - **Non-destructive first.** Prefer an adjustment layer over pushing a base slider to its
129
124
  limit: layers can be masked, faded with `opacity`, blended and stacked. Use base sliders for
130
125
  global exposure and white balance, layers for everything you may want to tune later.
131
- - **Order of corrections** (as in a raw / Photoshop workflow): exposure and white balance
132
- (`exposure`, `temperature`, `tint`) → black and white points and contrast (a `levels` layer,
133
- then a gentle S-curve `curves` layer) → colour (`vibrance` before `saturation`,
134
- `hueSaturation` per range, `colorBalance`) → local adjustments through masks → creative looks
135
- (`photoFilter`, `blackWhite`, `posterize`). Correct before you stylise.
136
- - **Avoid clipping.** Set levels black and white points (`master.inBlack`, `master.inWhite`)
137
- just inside where the image data starts (the histogram's `p1` / `p99`), not past it; large
138
- `exposure` or `contrast` values blow highlights and crush shadows in the output. More than
139
- about 0.5% of pixels in `clipping.highlightsPercent` or `shadowsPercent` on a normal photo
140
- means detail is lost; check bright skies, skin and deep shadows in `render_preview` too.
141
- - **Straighten, then crop.** Level horizons with `set_rotation` (small angles such as -2.5);
142
- any angle that is not a quarter turn leaves transparent corners, so crop inside them with
143
- `set_crop` afterwards. Keep a standard aspect ratio (3:2, 4:3, 16:9, 1:1, 4:5) unless asked
144
- otherwise, and do not crop away the subject's space to look or move into.
145
- - **Small moves.** Real corrections are usually within ±10..30 on these scales. Prefer
146
- `vibrance` over `saturation` for portraits (it protects already-saturated colours and skin).
147
- - **Contrast through curves.** A curves S-curve keeps endpoints fixed and avoids the flat
148
- greys that a big `contrast` value with low `exposure` produces. Curve points are levels
149
- 0..255 (`{ input, output }`); keep `(0,0)` and `(255,255)` and move quarter-tones by 10..25.
150
- - **Neutral greys.** Use `temperature` / `tint` (or `colorBalance` midtones) so known-neutral
151
- 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.
152
128
  - **Masks:** select, refine, then mask; see `pixerate-image-selection-masking`.
153
129
  - **Blend modes for tone-only or colour-only changes:** `luminosity` applies a curves or
154
130
  levels layer without shifting hue or saturation; `color` applies a tint without changing
@@ -159,29 +135,8 @@ in the source frame at the document's bit depth, colour mode and profile. Read
159
135
  natural colour mixing and painterly strokes; it is not a Photoshop mode, so keep it off
160
136
  layers that must survive a PSD round trip. It costs about ten times a normal blend.
161
137
  - **Name layers** (`name`) by intent ("Sky darken", "Warm skin"), so the stack stays readable.
162
- - **Effects: subtle first.** A drop shadow reads as natural with a small `distance` (2..10 px),
163
- a `size` a little larger than the distance, `opacity` 0.3..0.6 and the default multiply; a
164
- hard, dark, far shadow looks pasted on. Glows want `screen` and a light colour; strokes on
165
- text 1..4 px. Bevels: `depth` 1..2 and `size` a fraction of the shape's thickness.
166
- - **Outline-only looks** (an outlined title, a glass button): set the layer's `fillOpacity` to
167
- 0 with `set_layer_properties` and keep its effects; fill opacity hides the layer's own pixels,
168
- layer `opacity` fades the effects too.
169
- - **Prefer an effect over painted pixels** for shadows, glows, outlines and colour fills of a
170
- shape: it follows later edits and stays adjustable.
171
- - **Transforms: one resample, bicubic, whole pixels when you can.** Combine a scale, rotation and
172
- move into one `transform_layer` call; use `rotate_layer_90` / `flip_layer` (exact) instead of
173
- `rotate: 90` or `scaleX: -100`. `interpolation: "nearest"` keeps hard pixel art crisp; bicubic
174
- (default) is right for photos, bilinear is a little softer. Enlarging past about 200 % shows
175
- softness: prefer a larger source.
176
- - **Scale proportionally** unless asked to stretch: give `scaleX` and `scaleY` the same value.
177
- Keep the subject in frame: after a scale about `center` the layer can run past the canvas,
178
- which `bounds` in the result shows.
179
- - **Smart objects for anything you may scale again.** Convert first, then transform as often as
180
- needed; set exact sizes with `set_smart_object_transform` `scaleX` / `scaleY` (percent of the
181
- contents) rather than chaining relative scales. Prefer smart filters over layer filters when
182
- the user may want to mask, blend or reorder them later.
183
- - **Linked or not**: duplicate a smart object `linked: true` for repeated elements that must stay
184
- 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`.
185
140
  - **Merge Down only onto an opaque, 100 % lower layer** when the result must look the same:
186
141
  the merged layer keeps the lower layer's opacity and blend, so a faded lower layer fades the
187
142
  merged content too, and a blend mode on the upper layer stops seeing what was below the lower
@@ -209,6 +164,7 @@ in the source frame at the document's bit depth, colour mode and profile. Read
209
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. |
210
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. |
211
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. |
212
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. |
213
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`. |
214
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.