Commit Graph
20 Commits
Author SHA1 Message Date
aiongg baee303af1 Merge PR #5656: Image > Adjustments: locked smart objects refuse new smart filters (#5655) 2026-10-18 20:33:43 +00:00
aiongg 4127604122 Merge PR #6502: Engine: reject numeric overflow before document edits 2026-10-18 13:15:50 +00:00
aiongg 0a328189aa Smart filters, data sets, video: re-read stored files only when local or on the document's share
Paths these features keep in the document go through the same resolver as
linked smart objects (relative to the document's folder; other computers,
devices and URLs refused):

- A Displace smart filter's `mapPath` and a Color Lookup smart filter's LUT
  `file`, re-read on every re-render: a path that is not followed leaves
  the pixels as they are, as for a missing map or LUT.
- Apply Data Set's Pixel Replacement images.
- Reload Frame of a video layer's footage.

Apply Data Set and Reload Frame say why a path is not followed.
2026-10-16 16:53:28 +00:00
PerchAnton 3f216a0cd1 fix(engine): reject numeric overflow before document edits 2026-10-16 10:28:57 +03:00
David MannandClaude Opus 5.5 afd735ba96 Image > Adjustments: locked smart objects refuse new smart filters (#5655)
adjust_as_smart_filter recorded image.adjustments.* (and Shadows/Highlights)
as a smart filter without checking locks, so a Lock All or Lock Pixels smart
object still took the filter. It now refuses with check_pixels_unlocked, the
same rule filter.* smart filters use. Regression test covers both locks.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 19:57:05 +02:00
Gert Pellin - GPEandGitHub b007b1d355 Replace Color: Localized Color Clusters (points, localized, range) (#4477)
* Replace Color: Localized Color Clusters (points, localized, range)

image.adjustments.replaceColor takes the Localized Color Clusters of
Select › Color Range: "points" (where the colour was picked),
"localized" and "range" (% of the canvas's longer side). With clusters
on, the colour only matches near the points, through the same
color_range_samples as Color Range, so the same colour elsewhere in the
photo (a near-white background next to a cream garment) is left alone.
Without "localized" nothing changes. localized without points, and
malformed points, are refused.

The dialog part follows once the canvas eyedropper of #4247 is in.

Refs #4457

* Replace Color dialog: Localized Color Clusters and Range

The dialog gets Photoshop's Localized Color Clusters checkbox and its
Range slider (greyed while the box is off), and the canvas eyedropper
(#4247) now also records where the colour was picked (points), so the
localized match centres on that spot. The command's params list
localized, range and points as real keys in Photoshop's order.

Refs #4457
2026-10-13 12:47:47 -04:00
cee010ef5a Replace Color: a canvas eyedropper picks the From colour (#4143) (#4247)
The dialog had no colour control at all: the engine defaulted the From
colour to the foreground and the spec's `color` param rendered nothing.
It now shows a Color row (swatch opens the Color Picker, seeded from the
foreground) plus a pipette that arms a canvas eyedropper: the next click
on the image samples the merged composite into the field, like the
Curves picker and Photoshop's Replace Color.

Co-authored-by: highperfocused <highperfocused@pm.me>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 22:03:15 -04:00
1e9f1ae486 perf(engine): read selection coverage per row when mixing edits back (#4424)
Destructive adjustments (pixels::adjusted) and the RGBA filter helper (adjust_cmds
process_surface) each had the same loop mixing the edited pixels back over the original by
selection coverage, looking the selection's tile up for every pixel (sample_channel, a
BTreeMap lookup per pixel). They now share pixels::mix_through_selection, which reads the
coverage one row at a time with sample_row_strided: one tile lookup per tile per row, and a
row-sized buffer.

Results are unchanged: a new test compares the helper with the per-pixel sample_channel mix,
bit for bit, for 8-bit, 8-bit-with-alpha and 32-bit selections over 8- and 16-bit layers,
across tile edges and untouched (default) selection tiles.


Claude-Session: https://claude.ai/code/session_01QUda3K786uop8kvEQoUjSf

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-12 19:55:21 -04: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
4c06b9fe4d fix(engine): adjustments on a smart object record smart filters (FILE-215-4); strokes honour doc CMYK (#2229)
On a smart-object layer, Image > Adjustments now records an editable
smart filter instead of committing disaster.

  - new engine helpers:
    - adjust_cmds::has_adjustable / adjust_as_smart_filter route
      image.adjustments.<kind> onto the source, with the selection as the
      filter mask, through smart_cmds::add_smart_filter
    - brush/eraser/pencil/live preview strokes now render inside
      with_cmyk_space(doc_cmyk_space(session)), so painted colours follow the
      document profile the same way the compositor does
  - docs/scorecard.md: FILE-215-4 partial -> done
  - tests: adjust_cmds/tests + brush_cmds/tests, plus panic_hunt and
    the lib suite all green on the committed tree

Known follow-up, already filed:
  - FILE-215-5 rows: double-clicking an adjustment smart-filter row is still
    a no-op. smart_ui::open_editor only proceeds when
    filter_dialog::has_dialog returns true, and has_dialog accepts
    filter.*, a fixed non-adjustment set and PREVIEWED - but not
    image.adjustments.* (those open through adjust_dialog::open, which isn't
    wired into the smart-filter row).

This is one commit on top of the latest upstream/main (rebased to fix the
stale fork ancestry that was blocking PR creation).

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 05:49:07 -04:00
1284e79d23 Selective Color and Color Lookup: a wrong-typed key is an error (#1790)
Selective Color and Color Lookup params with the wrong type (a string percentage, a short range
array, an unknown method, range or interpolation) were silently ignored, so layer.setAdjustment
kept the old values without saying so. They are now BadParams errors naming the key; null still
means not given. Fixes #936.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 09:23:34 -04:00
buivanvu94andBFlat e80a691ecf fix(engine): report invalid index on document.inspect and document.activate (#1557) 2026-10-09 13:24:03 +07:00
buivanvu94andBFlat 1ddda05394 fix(engine): use grammatical article for adjustment layer error messages (#1549) 2026-10-09 13:24:03 +07:00
53985e5f34 Color Lookup: keep the .cube input domain and reject the ones we cannot apply (#1130)
* Color Lookup: keep the .cube input domain and reject the ones we cannot apply

DOMAIN_MIN/DOMAIN_MAX (and LUT_3D_INPUT_RANGE) were parsed and thrown
away, so a log-domain or extended-range look was applied to plain 0..1
inputs: silently wrong colours, exactly what the in-code comment
admitted. The domain now travels with the parsed LutFile (validated:
every component finite and min < max), and colorLookup refuses a
non-default domain with the reason instead of shifting the look -
the same unsupported-over-wrong-output call the codecs make. Honoring
a custom domain end to end means carrying it through ColorLookup
(doc, compose, gpu, PSD round trip) and stays open as the follow-up.

* tests: the colorLookup domain rejection needs a pixel layer active

The outer session of the test still had the adjustment layer active, so
the command was disabled before reaching the domain rejection - the
assertion only passed when the active layer happened to differ. Make it
deterministic with a fresh pixel layer.

* Color Lookup: bake a 1D .cube's input domain into the resample

The 1D branch of parse_cube still ignored DOMAIN_MIN/MAX and
LUT_1D_INPUT_RANGE. Since the 1D table is resampled onto a 0..1 cube anyway,
the domain is now applied there (saturating outside), and a degenerate
domain is rejected like the 3D one.

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

* cargo fmt after rebasing onto main

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 19:41:07 -04:00
a93fb88064 Invert (⌘I) inverts the targeted layer mask (#780) (#838)
* Invert (⌘I) inverts the targeted layer mask (#780)

With the layer mask targeted (its thumbnail clicked, or viewed with
Alt-click), Image > Adjustments and the filters now edit the mask instead
of the layer's pixels, as in Photoshop. Without a selection Invert also
flips the mask's default, so a reveal-all mask becomes hide-all, past the
canvas too. A targeted mask enables these commands on layers without
pixels (adjustment and fill layers), and dialog previews show the mask.

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

* rustfmt

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:30:53 -04:00
2873afdda9 fix(layers): a locked group locks the layers inside it (#847)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-07 20:18:27 -04:00
Brandon ThomasandGitHub eaf92dcd9d Smart Objects and Smart Filters survive PSD save and open (#288) (#288) 2026-10-06 07:46:37 -04:00
4218d07f40 Never crash: the engine is panic-free (Rule 9) (#54)
Every unwrap/expect/unreachable in non-test engine code now returns an
EngineError (or a predicate's Err string) or uses a real fallback:
- enabled predicates read the active layer through active_layer_of()
  instead of unwrap() after has_layer();
- new raster layers get their pixels through pixels_mut()? instead of
  surface_mut().expect("raster");
- layer paths use split_last().ok_or(..)? (Merge Down, Arrange, Ungroup,
  Release Clipping), Auto-Align/Auto-Blend/Photomerge carry placements
  instead of re-unwrapping them, and "next free name" searches are bounded;
- web export builds its options map directly, quantize_rows has a wasm
  tail, parse_script rejects non-array/object JSON, the shape-layer edit
  returns an error instead of unreachable!, and the gray composite
  profile falls back to sRGB.
The engine denies unwrap/expect/panic/unimplemented/todo/unreachable.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:26:26 -04: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