# PhotoCraft MCP: capability summary

*Probed 2026-10-15. PhotoCraft runs headless, with 21 MCP tools and about 860 engine commands. Every claim below was tested in this session.*

1. **Procedural:** vector shapes (rect, ellipse, polygon, star, Bézier paths with combine/subtract), live gradient and pattern fill layers, render filters (clouds, fibers, tree, lighting, lens flare), noise and blur filters, and brush strokes from point lists. There is **no scatter or particle engine**, and script files are disabled in automation.
2. **Type:** a real type engine. **Live type layers survive PSD round-trips**, with tracking, kerning, OpenType features, paragraph boxes and live warp (arc). OFL fonts are installed: Linux Libertine G/Display G, Inter, Noto Serif.
3. **Text on a curve:** live only as warp (arc/arch). True text-on-path works only through SVG `textPath` import, and it arrives as outlines, not live type.
4. **Vectors:** shape layers keep editable Bézier paths. Type converts to shape outlines. Vector masks, clipping masks and smart objects work. Mesh warp works through `edit.transform.warp`; `layer.smartObjects.warp` is disabled. The can wraps rely on it.
5. **SVG export: no.** `pathsToIllustrator` (.ai) is disabled in automation, so logo SVGs are generated by my own code from the same geometry.
6. **SVG import: yes.** It becomes **named, editable shape layers** (SVG `id` → layer name). This is how generated geometry enters PhotoCraft.
7. **Export:** PSD, PNG, JPG, TIFF, WebP and native .pcraft all work. **PDF does not** (unknown format; Print and Artboards→PDF are disabled in automation).
8. **Import:** PNG, JPG, WebP, SVG and PSD all open. The reference can image (1536×1024) opened fine.
9. **Non-destructive:** adjustment layers (curves, color balance, hue/sat, gradient map, photo filter, LUT), layer styles, and smart objects with **live smart filters**. All of these were verified to survive PSD save and re-open.
10. **Sandbox:** writes go only to `Poli-all/image-work/`, and reads need absolute paths there. Masters are built there and mirrored into this folder.

## Gaps and the closest achievable version
| Brief asks for | PhotoCraft can't | What I do instead |
|---|---|---|
| Procedural bloom/field "in PhotoCraft" | No scripting or scatter engine reachable | `aster.py` (seeded, parametric) emits named vector layers. PhotoCraft imports them and does all the finishing: depth blur as smart filters, haze fills, light grading as adjustment layers, grain. |
| Logo `.svg` | No SVG export | PhotoCraft sets and outlines the wordmark and saves it to PSD. `tools/psd_paths.py` reads the exact Bézier paths back from the PSD's vector masks, and `build_logos.py` writes clean SVG from them (fontTools only for the seal's ring text). |
| `.pdf` for print pieces | No PDF export | Flattened 300-ppi PNG from PhotoCraft (100 ppi for the 72 × 30 in banner), then a PDF at true physical size (`tools/make_pdf.py`, PIL). Die and fold lines go on a second proof page. It's a raster PDF, so ask the printer whether they want vector type. |
| Hand-drawn brushwork | Headless, so no pointer | `paint.stroke` with generated point lists, where texture is needed. |

## Alpha issues found during Phases 0–2 (and what I did)
| Issue | Impact | Handling |
|---|---|---|
| `select.findLayers` matches **substrings** | `depth-5` also grabbed `haze-depth-5`, which got merged into the smart object | Names are designed to be collision-free (`haze-5`, `c01-…`) |
| Gradient fill layers hold their end values **beyond** `from`/`to` | Haze layers washed out the whole sky | Every haze ramp starts and ends at 0% |
| Gradient fills are stored **relative to canvas size** | Cropping a document distorts them | Never crop a master; check crops in a flattened export |
| Embedded smart objects re-open as **"linked" by UUID** | Cosmetic in PhotoCraft (renders identically); other apps may differ | Reported. Untested in Photoshop |
| SVG `opacity` on an element creates an **unnamed wrapper group** | "Group 1…10" in the can lineup | Renamed (`c01-shadow-group`…); the checker flags any generic names |
| `render_preview` **downsamples with aliasing** | Smooth vector edges *looked* wobbly | Critique edges at 1:1 or on an exported PNG |
| Image Size on vector layers re-renders them crisp | A "nearest-neighbour 8×" of a 16 px icon wasn't pixelated | 8× views are made from the flattened 16 px PNG |
| No PDF writer, no SVG writer, no script runner in automation | See the gap table above | Workarounds listed above |

## Alpha issues found during Phases 3–5 (and what I did)
| Issue | Impact | Handling |
|---|---|---|
| A custom warp with **more than one patch is dropped** on PSD save (with a warning) | The can label would re-open flat | Can wraps use a single-patch warp, with smoothstep x-mapping and a 0.04 w sag. Re-opened and confirmed |
| `file.placeEmbedded` / `placeLinked`, `layer.smartObjects.warp` and droplets are **disabled** | No direct "place image" | Open the image as a document, then `layer.copyToDocument {at}`, convert to a smart object, and scale with `edit.transform {matrix}` |
| `copyToDocument` lands **above the destination's active layer**, not on top | A mark landed under the grade layers | Select the target layer in the destination first |
| `select.allLayers` **skips a Background layer** | A copy from an opaque PNG copied nothing, and later steps hit the wrong layer | Copy opaque PNGs without selecting first, and target layers by id (`layer.select`) |
| A batch **stops at the first failure**, e.g. `bringToFront` on a layer that's already on top | Half-finished batches | Recipes avoid no-op arrange steps; results are checked after every batch |
| `doc_close` takes **only `index`** (no `save`). Commands act on the **active** document | One 3/4 recipe ran in the wrong file | Close by index, `doc_select` before each batch |
| **Image Size + a new resolution** in one call scales type and gradients by the ppi ratio | The first 18 × 24 poster had giant type and a peach sky | Never mix them. Resample at 72 ppi, then tag ppi with `resample:"none"` |
| **Image Size shifts gradient-fill offsets** and **doesn't scale smart-filter radii** | Gradients slid off; depth blur weakened | After a resample, re-point every gradient with `gradient.fill.set` and scale blurs with `layer.smartFilter.setParams`, as done on the banner. The poster is rendered natively at size instead |
| `gradient.fill.set` needs the fill to be the **active** layer | "not a gradient fill layer" | `layer.select {layer:id}` first |
| A save estimated **over 2 GB is written as PSB** under the .psd name | A wrong extension for Photoshop | Renamed `.psb` (the poster) |
| A smart object with **huge embedded vector contents** is written as pixels | The poster's two farthest field bands are pixel layers in the PSB | Documented; everything else stays live |
| Opening an **85 MB SVG** takes over 30 minutes (the tool times out, the document still opens) | A native 300-ppi field is slow | Accepted for the poster. The banner uses resample + repair instead |
| `findLayers` substring matching, again: `shelf_talker-mark` matched `talker-` | A grouping step swallowed the background | Layer prefixes on every piece are collision-free (`brand-`, `price-`, `chai-stage`…) |

**Verification**
- **Phases 0–1:** 32 PSDs were re-opened through the MCP, and `tools/verify_report.txt` records the result.
- **Phases 1–5 additions:** 67 new PSDs and PSBs were re-opened through the MCP (`doc_open`). Every layer count matches the count read from the file bytes, and the poster PSB was also inspected with `doc_inspect` (groups, live type, 300 ppi).
- **Parser check:** `tools/verify_all.py` reads all **112** files directly (99 plus 13 renamed copies in `01_logo/approved/`). Report: `tools/verify_all.txt`. 0 issues: no generic layer names, no "Astor", none of the ruled-out claims, no Wisconsin, dairy or local copy, no hype words, and "8 FL OZ (237 mL)" as live type on every label and lineup.
- **Copies:** the project copies are byte-identical to the re-opened build files (`tools/verify_reopen_phase2-5.json`).
