Photoshop's Background is the bottom layer of a file without transparency: a pixel layer that is
transparency- and position-locked, whatever the file's language calls it ("Hintergrund", "背景",
"Arrière-plan"). We matched the name "Background" in compose (deep / shallow knockout), the
engine (cut/clear, merge visible, transforms, warps, distort, erasers, styles, masks, crops,
straighten, smart-object unpack) and the UI (Properties, layer row, tab title), so a localized
file's deep knockout revealed transparency instead of the Background.
- `Document::background` / `background_mut` / `is_background` and `Layer::has_background_traits`
in the doc model replace every name check; `extra_cmds::background_to_layer` is the one
"Background → Layer 0" conversion.
- PSD import marks the bottom record position-locked when it is transparency-protected and has
no transparency channel, with or without an `lspf` block. Affinity import and HDR Toning now
lock the Background's position like every other place that creates one.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A Displace applied to a smart object is recorded as a smart filter and
rendered by apply_smart_filters -> filters::apply_filter_to_surface, both
when it is added and on every later re-render (replace contents, toggling
a filter, transforms). That path built FilterParams with params_for alone,
which leaves Displace's map as None; only run_filter resolved `mapPath`
(through filters_ext::resolve, which needs the session). With no map,
algo's displace returns the source unchanged, so the smart filter was a
silent no-op.
apply_filter_to_surface now resolves `mapPath` through the same file read
and import the direct command uses (filters_ext::map_from_path, shared by
resolve and the new session-free resolve_map_path). A map that can no
longer be read on re-render leaves the pixels alone, as for an unknown
filter; the command still refuses an unreadable map when the filter is
added. `mapDocument` needs the session and stays unresolved on re-render.
Regression test: smart_cmds::tests::
displace_smart_filter_reads_its_map_path_on_re_render (8/16/32-bit).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Showing, hiding or editing a smart filter re-rendered the whole stack from the
placed source on the UI thread: on a 3000x4498 layer with five filters every
click on a filter's eye froze the window for about 1.5 s with no feedback.
- Each render keeps the placed source and the result after every visible filter
(blake3 of the source contents, the placement and the filters so far), so an
edit reruns only the filters from the edited one up. Edit > Purge > All drops
them.
- Smart filter edits apply the edit at once (so bad params still fail
immediately) and re-render as a background job when started with
Session::start: the window keeps drawing, with progress and Esc to cancel
between filters. Session::execute is unchanged.
Convert to Smart Object with several selected layers wrapped them in a
temporary group before embedding, so Edit Contents showed one group named
after the top layer. Photoshop puts the selected layers at the top level of
the embedded document. The embedding now goes through layers_to_smart, which
takes the layers directly; a pass-through root composites the same, so the
smart object looks unchanged. A single selected group still converts as is.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The embedded document built by layer_to_smart now carries the document's patterns, so pattern fills
and pattern effects keep rendering inside the smart object and after Edit Contents. Fixes#1867.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The refusal message Debug-quotes the path, which doubles backslashes on Windows, so
contains(path) never matched there. Matching the unique file name keeps the check.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Fixes#1833. Control and MCP may run Edit Contents, Convert to Layers and Save Contents on smart objects whose source is in memory (embedded, or a Photoshop UUID backed by embedded liFD data). Before any disk read the engine re-checks the source's stored path through the session's authorize gate, including nested sources re-rendered while unpacking, so external linked files stay refused; explicit layer targets work with another layer selected, and interactive linked-file access is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
document.inspect (and photocraft-cli info) now gives every smart object a
smartSource: embedded with its file name, or linked with a file path or,
for a PSD placed layer, the Idnt uuid its duplicates share. A script can
then tell duplicates of one placed file from independent contents, e.g.
to check that every location in a mockup template has its own art.
Co-authored-by: Juan Manuel Verges Solanas <jmverges@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Edit Contents switches to the already-open contents document
Opening a smart object's contents a second time opened another copy.
It now activates the document already linked to that smart object.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* rustfmt smart_cmds.rs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Jason Short <3344856+jason4short@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Layer > Smart Objects > Convert to Layers (#535)
Unpacks a smart object's contents back into layers at its placement:
several layers land in a group named after the smart object (carrying
its opacity, blend mode, masks and style); a single layer replaces it
directly when those fold in exactly. Contents are conformed to the
document's mode, profile and depth; smart filters are discarded.
Also in the Layers panel context menu and the Properties quick actions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Parity: Convert to Layers is live (627 of 627); raise FLOOR
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Convert to Layers: pass no inherited group locks to transform_layer (#847 added the parameter)
The layers are transformed inside released(), which lifts every lock in their tree.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* it: translate Convert to Layers (Italian landed after this PR was written)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Smart objects keep their four-corner (Distort / Perspective) placement
A smart object's placement was an affine transform, so a perspective
placement lost its fourth corner: a mockup PSD's distorted screen looked
right on open (Photoshop's cached pixels) but re-rendered as a
parallelogram after Edit Contents → Save.
- SmartObject gains `perspective`, the full projective map (source px →
document px); `transform` keeps its affine approximation
- Re-renders go through it (place_source_projective), as do moves,
canvas rotations and edit.transform, so Distort / Perspective now work
on smart objects instead of being refused
- PSD import reads the quad (nonAffineTransform / Trnf) and export writes
it back; .pcraft stores the field (absent for affine placements)
- Test: a distorted smart object survives Edit Contents → Save
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Smart objects: warp maps through a distorted placement; perspective round-trip tests
Warp handles and stored warps on a Distort / Perspective smart object go through its full
projective placement (smart_cmds::placement) instead of the affine approximation, so they sit
on the distorted corners. Tests: SoLd perspective round trip and quad_moved (io), a perspective
smart layer in the .pcraft rich_doc, warp on a distorted smart object (engine). Comments no
longer name another product.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Jason Short <3344856+jason4short@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Gradients honour explicit transparency stops with colors, a preset or the current gradient (#465)
`paint.gradient` and `gradient.fill.create` dropped `transparency` unless `stops` was
given. Explicit transparency stops now replace the gradient's own opacity stops whatever
supplies the colours. One shared parser (`transparency_param`) serves paint.gradient,
gradient.fill.create/set and the presets; malformed stops are an error, not dropped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Smart filter commands check an explicit layer target, not the active layer (#466)
`layer.smartFilter.*` with `"layer":id` were refused when the active layer was not a
smart object with filters, although they edit the named layer. Their precondition is now
checked on that layer; the active layer stays unchanged, and without `layer` (menus)
nothing changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Gradients: reword the opacity-merge doc comment
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
CI's `cargo fmt --all -- --check` has failed on every run because the
repo had no rustfmt config and was never formatted. Use the same settings
as EffectCraft/FilmCraft (max_width 160, use_small_heuristics Max), which
also keeps the churn small. Formatting only; no code changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>