Commit Graph
41 Commits
Author SHA1 Message Date
aiongg 2496742eaa Integration: merge main
Merges main through #3283 (Smart Filters: distinct editors, shared
disclosure and stable alpha).

filter_dialog.rs: main's split of the preview into is_smart_edit,
smart_preview_factor and scale_preview_params is kept. The plug-in branch
from #7524 (plugin_cmds::scale_for_preview scales only declared pixel
lengths) moved into scale_preview_params, so it applies to a plug-in run
on its own and to a plug-in recorded as a smart filter.

smart_ui.rs open_editor: main's smart_filter_dialog::target flow and the
shared adjust/filter dialog opening are kept; the Liquify re-open with
recorded strokes (r4) stays ahead of it. The old per-adjustment
edit_filter call is dropped with main's rewrite.
2026-10-22 15:39:28 +00:00
aiongg 537d29074d Merge PR #6880: Disable Convert to Smart Object for layers that are already smart objects 2026-10-22 02:22:01 +00:00
aiongg 3458eee861 Merge main into #3283
dialogs.rs: main's Displace-map prompt runs before the fallible confirm_command. filter_dialog.rs: main's ask_displace_map uses the new confirm_command signature; SMART_FILTER_SET_PARAMS stays for main's uses; a smart-filter edit without a "layer" param previews the active layer, as the command runs it (main's smart_displace_preview_uses_the_map_document).
2026-10-22 02:16:49 +00:00
abc2cbandClaude Opus 5.5 6da0673cd1 Merge upstream/main 6d8cbac7 into the Smart Filters branch
Conflicts:
- filter_dialog.rs: preview_document_for_target keeps our proxy scaling
  and `p` clone; main's Bitmap rule (the preview drops Output resolution,
  #5559) is applied to the same `p`.
- hu/sr/tr/vi catalogs: both sides appended rows at the end; kept both, no
  duplicate keys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 00:14:11 +03:00
Matthias Schlich d104f7015f fix(filters): Clouds and Fibers move with their smart object
Their noise was sampled in document coordinates, so moving a smart object re-rendered a
different texture with the same params. The pattern is now anchored at the filter's bounds:
the object for a smart filter, the canvas on a layer. Fibers, like Clouds, takes the canvas
as its bounds even with a selection, which only masks it, so layer results stay as before
(CodeRabbit review on #6160).
2026-10-20 22:49:04 +02:00
Matthias Schlich 0a7325af6e Merge origin/main into feat/psd-render-smart-filters
# Conflicts:
#	docs/file-format-parity.md
2026-10-20 22:20:37 +02:00
abc2cbandClaude Opus 5.5 25b79243f1 Smart Filters: document the functions next to the ones the PR changes
CodeRabbit counts the functions around the lines a PR changes, not only the
changed ones, so the PR's docstring coverage stays low (59.49%, threshold 80%)
unless the untouched neighbours are documented too. Comments only; no code
changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 17:54:43 +03:00
abc2cbandClaude Opus 5.5 79793acefe Smart Filters: document the functions and tests the PR adds
CodeRabbit measured the docstring coverage of the PR at 59.49%, under its 80%
threshold. Add short doc comments saying what each helper does and which
behaviour each test pins. Comments only; no code changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 17:50:28 +03:00
Matthias Schlich c6fff8a3a8 Merge origin/main (up to a1e19a0b) into feat/psd-render-smart-filters
# Conflicts:
#	crates/engine/src/smart_cmds.rs
2026-10-17 12:10:28 +02:00
NetterSilverandClaude Haiku 5.5 2466f5c3a7 Disable Convert to Smart Object for layers that are already smart objects
Fixes #6730

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 01:52:06 +02:00
David MannandClaude Opus 5.5 e5f1ecf501 Merge main into fix/smart-filter-last-mask
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 22:24:50 +02:00
abc2cbandGitHub 0ab262a0e3 Merge branch 'main' into codex/github-smart-filters-usability-20261011 2026-10-16 12:20:31 +03:00
skylar taylor-barrickandGitHub b630d7cb9a Security: enforce the automation read boundary on stored smart-filter params (#5684)
* Security: enforce the automation read boundary on stored smart-filter params

Smart filters store a command id and params inside the document, so a
crafted .pcraft could resolve stored ambient paths (displace mapPath,
color-lookup LUT files) at render time without the authorize gate:
synchronous smart-object rendering has no Session argument, spawned job
threads drop the requesting session's authority, and a trusted UI
render could warm the process-wide stack cache with a stage the gate
never saw.

Thread the requesting session's gate through a thread-local in jobs.rs
(entered around command execution, reinstalled on spawned workers and on
job apply, restored on drop), re-check stored filter commands in
render_in before source resolution and any cache lookup and in
apply_filter_to_surface, and re-authorize layer.smartFilter.setParam
with the complete merged params after merging stored document params —
including edits whose request contains no path and actions.play steps.
Denial is conservative: the document is unchanged and no worker starts.

Regression tests in the automation crate cover rerender, cached-stage
reuse, merged-param edits, action playback, background jobs, and filter
transfer, for both the displace map and color-lookup LUT vectors.

* Review: carry render authority through Session::edit and surface denials

Session::edit is the engine's universal mutation entry (linked-file
refresh, transforms, place); renders reached through it ran with no
authorization scope installed, so stored smart-filter params read
ambiently there even after the execute-path fix. Install the scope in
Session::edit as well, keyed on the session's authorize gate.

apply_filter_to_surface also conflated a denied filter with an unknown
one: authorize_render_command's Result was discarded with .ok()?, so a
denial silently rendered unfiltered pixels and the stage entered the
process-wide stack cache. The signature becomes
Result<Option<Surface>>: Err for denials (callers propagate), None for
ids that aren't pixel filters (pass-through semantics unchanged), and an
unreadable map keeps its deliberate pass-through. apply_smart_filter and
apply_smart_filters propagate instead of cur.clone().

Two tests: a refresh driven outside command execution (Session::edit)
is denied and leaves the document unchanged; a denied render caches no
stage. Nine authorization tests pass, engine suite and strict Clippy
clean, panic hunt green.

* ui_set description: name the layerFilter field main added

The Layers-panel filter dropdown (#5048) added layerFilter to the
control method's UI_SET_FIELDS without extending the ui_set tool's
description, so the drift guard (mcp.rs, ui_set_description_names_every_
accepted_field) fails on Windows CI for any branch built on main. Name
the field with its patch shape: by (kind, name or mode), kinds (a list
of layer kinds), name (matches part of the layer name), mode (a blend
mode name). The 40 MCP tests pass; strict Clippy clean.

* wasm size: compose and algo join cms at opt-level "z" to clear the 24 MiB gate

Main's browser build is 171 KB over the 24 MiB wasm gate in CI
(25,337,343 bytes), so every branch built on current main fails the
web size check. Demote three pixel crates in the wasm-release profile:

- cms: transforms are built once and cached (transform::cached),
  so "z" costs little runtime (measured -42 KB).
- compose: the browser app composites per frame on the GPU (crates/gpu,
  WebGPU/WebGL2), so the CPU compositor is not the browser's hot path
  there (measured -122 KB).
- algo: filters run per pixel on every interactive filter application.
  "z" there is the lever of last resort (measured -331 KB), taken
  because the gate is enforced in CI; if browser filters feel too slow,
  restore 3 here and reclaim the bytes from a feature-gated decoder.

Final size 24,811,949 bytes locally, 354 KB under the gate; the native
profiles are untouched. If a maintainer prefers keeping algo at 3, the
alternative is raising the self-imposed 24 MiB early-warning limit
(Cloudflare's hard cap is 25 MiB) - a policy call in packaging/web/
package.sh, not a code fix.
2026-10-15 18:19:26 -06:00
Matthias Schlich 50a011ae9e feat(filters): Clouds and Difference Clouds as smart filters; render filters work within a smart object
Clouds and Difference Clouds ran outside the filter pipeline (map_pixels with the session's
colours), so on a smart object they failed instead of adding a smart filter. They are now a
regular render filter (FilterParams::Clouds, same noise as before) that records its colours,
so a smart filter re-renders the same clouds after the colours change.

Render filters on a smart object (Clouds, Difference Clouds, Fibers, Lens Flare) now use the
placed content's bounds, as Photoshop does: a flare's centre is a fraction of the object, not
of the canvas.
2026-10-15 16:05:34 +02:00
abc2cbandClaude Opus 5.5 d16b4f7302 Merge upstream/main (8d840eab) into Smart Filters editors
Resolve smart_ui.rs: a Filter Gallery smart filter opens main's gallery
window (#3426) before this PR's editor routing; catalogs keep main's
hu/sr/tr/vi rows and this PR's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 23:58:05 +03:00
David MannandClaude Opus 5.5 e29ec76d4c Smart Filters: deleting the last filter removes its filter mask (#5619)
Deleting or dragging away the only smart filter now clears the filter mask, as
Clear Smart Filters already did, so the next filter takes the new selection.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 18:57:40 +02:00
706638b284 Tests: bad-layer-id checks use an id that can't exist (#3343) (#3351)
* Tests: bad-layer-id checks use an id that can't exist (#3343)

Layer ids come from one process-wide counter (shared with document and
smart-object ids, and bumped by ensure_ids_above on file loads). A full
`cargo test` run passes ids like 9999, so a layer a test just created can
get the "bad" id, the command succeeds and the assertion fails at random
(seen on CI in type_extra_cmds::tests::vertical_orientation_renders_columns_inside_the_canvas).

Use u64::MAX, which the counter never reaches, for every "no such layer"
sentinel in the engine and ui-egui tests. Test-only change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Tests: use u64::MAX for bad-layer-id sentinels added on main since (#3343)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 01:59:43 -04:00
abc2cbandClaude Opus 5.5 f43f9df2d7 Merge upstream/main (74cb2935): keep main's hu/sr/tr rows and this PR's
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 19:55:39 +03:00
Gert Pellin - GPEandGitHub 1bac788706 info: a smart object embedded in a PSD is reported as embedded (#4843)
PSD import keeps every placed layer as SmartSource::Linked { path:
<Idnt uuid> } so duplicates share the file and export stays
byte-stable, and info reported that as a link whose path is the uuid.
When the uuid has an embedded file (a liFD item in the preserved
lnk2/lnk3/lnkD blocks), the document's info now says
{"kind": "embedded", "fileName", "uuid"}; the new
linked::embedded_file_name finds it without copying the file.

Fixes #4842
2026-10-13 12:47:01 -04:00
Gert Pellin - GPEandGitHub 663863e62b Smart Objects > Export Contents: another extension exports the contents in that format (#4847)
Export Contents wrote the smart object's file as it was whatever the
path, so out.png held .pcraft or .psb data. With the contents' own
extension (or none) it still copies the file; with another one it opens
the contents and exports them in that format, as File > Export does
("converted": true, with the export's warnings). jpg/jpeg and tif/tiff
count as the same; an unknown extension is an error.

Fixes #4846
2026-10-13 12:46:40 -04:00
abc2cbandClaude Opus 5.5 641155b3bc Merge upstream/main (cdfec72f) into Smart Filters editors
Resolve the conflicts with main's smart filter row selection, drag and
reorder (#4371, #4897), channel-targeted filter previews and the new
hu/tr/vi rows:

- smart_ui.rs: filter rows keep this PR's layout (name and Blending
  Options icon as separate targets, indents) and take main's click to
  select, drag and drag-to-reorder; the name forwards them to the row.
  This PR's widget tests move to smart_ui/editor_tests.rs next to main's
  inline unit tests.
- filter_dialog.rs, canvas.rs: the smart filter preview scaling and
  smart_preview_factor run inside main's preview_document_for_target,
  with its channel target and source documents.
- i18n: main's rows plus this PR's; hu rows use informal address (#4334).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 19:39:11 +03:00
F4LCONandGitHub 05e98e7ade Smart Filters: select a filter, drag or ⌥-drag it onto another smart object (#4271) (#4371) 2026-10-12 22:35:54 -04:00
f6db7a8115 Smart Objects Warp: mesh knots span the contents, not the skewed bounding frame (#4113) (#4341)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 21:27:03 -04:00
abc2cb 2a54c3a573 Merge upstream main into Smart Filters editors 2026-10-13 01:10:05 +03:00
b564f2ca9c fix(layers): identify the Background by structure, never by its English name (#4036)
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>
2026-10-12 07:04:33 -04:00
2cb396d154 fix(engine): Displace smart filter ignores its mapPath displacement map (#3676)
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>
2026-10-12 06:28:33 -04:00
abc2cb 1515cafae4 fix(engine): retain Smart Filter coverage around blur edges 2026-10-11 10:31:23 +03:00
abc2cb f9f1d823cc Make Smart Filter editors distinct and preserve alpha at partial strength 2026-10-11 09:59:42 +03:00
twentymlsandGitHub d3ae5aad0f Smart filter edits: re-render from cached stages, in a background job (#2739)
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.
2026-10-10 17:38:59 -04:00
9bd6b8111e Smart Objects: converting several layers keeps them at the contents top level (#2648) (#2651)
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>
2026-10-10 16:10:36 -04:00
3bc49f5e1a Fix Smart Object conversion of selected layers (#1079) (#2052)
Co-authored-by: zenjabba <679864+zenjabba@users.noreply.github.com>
2026-10-09 14:03:16 -07:00
54d7b07acf Convert to Smart Object keeps a pattern fill's pattern (#1937)
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>
2026-10-09 15:33:20 -04:00
ee3ebfa748 Smart contents test: match the refused path by file name (Windows CI red since #1882) (#2042)
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>
2026-10-09 15:32:39 -04:00
9e36825d2d Allow automation to edit embedded Smart Object contents (#1882)
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>
2026-10-09 14:24:58 -04:00
2b139ebd5c inspect: report each smart object's source (#1785)
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>
2026-10-09 22:08:07 +07:00
7e40b40fb0 Edit Contents switches to the already-open contents document (#1037)
* 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>
2026-10-08 15:41:19 -04:00
01cd6fffa2 Layer › Smart Objects › Convert to Layers (#535) (#829)
* 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>
2026-10-07 23:44:21 -04:00
5856866f89 Smart objects keep their four-corner (Distort / Perspective) placement (#426)
* 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>
2026-10-07 06:58:01 -04:00
faaa42db95 Gradients honour explicit transparency stops; smart filter commands accept an explicit layer target (#465, #466) (#544)
* 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>
2026-10-07 13:21:22 +07:00
Brandon ThomasandClaude Opus 5.5 f7eb6ea9e7 Format: adopt the craft-family rustfmt.toml and format the workspace
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>
2026-10-04 11:31:17 -04:00
Brandon Thomas 536a7346fb more algorithms, progress 2026-10-01 08:25:06 -04:00