The pixel-only cap (2^27) still let two RGBA layers past the canvas with
about 11 GiB of working buffers. Count what Auto-Blend holds per pixel
(each layer's pixels, luminance, alpha and weight, plus the merged picture
and depth map) against a 4 GiB budget; over it, keep to the canvas as
before. The per-layer region reads and luminance/alpha buffers now use the
fallible allocation helpers, so running out of memory is an error.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A sparse layer (a dot at each far corner) has huge bounds that also raise
the content estimate, so the whole union was chosen and its buffers could
exhaust memory. Past the canvas, work on at most MAX_BLEND_PIXELS; beyond
that keep to the canvas as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Per review, the canvas-wide area, Seamless gain compensation and Background
conversion move to their own PRs. Adds an engine test for the crop case.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Auto-Align warped a moving layer's mask with the colour warp (warp_placed), which
returns a grey + alpha surface defaulting to 0, so a reveal-all mask hid everything
outside its painted area. Masks now go through warp_gray (planar placements) or
warp_mesh_gray (cylindrical/spherical), keeping one channel and the mask's default.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Panorama seams skipped an image lying wholly inside the composite, so after
Auto-Align moved the second photo partly off the canvas it got an all-black
mask and nothing changed. Such an image now takes the side of the seam it
lies towards; blending covers the layers past the canvas; Seamless Tones and
Colors evens out exposure; the Background becomes a layer to take its mask.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* 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>
* Auto-Blend Stack Images: a depth map by depth from focus, for Lens Blur
`edit.autoBlendLayers` with `method: stack` and `depthMap: true` adds a hidden gray Depth layer, the layer each pixel is sharpest in (black = the bottom layer, white = the top), from `photocraft_algo::dff`: the ring difference filter as focus measure, guided-filter aggregation with the merged picture as guide, a streaming peak search with Gaussian interpolation and a confidence, edge-aware weighted least squares solved by multigrid-preconditioned conjugate gradients, and guided upsampling. Once on a layer mask it drives Lens Blur's depth map.
Closes#2067.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Depth from focus: a guide or plane of the wrong length is the empty map
Review follow-up on #2073: `depth_from_focus` is public and indexed
`guide[i]` without checking that `guide` and every `luma` plane are
`w × h` long. It now returns the empty map (depth 0, confidence 0) for a
mismatch, before anything is read, and the test covers a short guide, a
short plane and a size that matches neither.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* Auto-Align Layers: register focus and exposure brackets by intensity when there is nothing to match
Feature matching fails outright on layers that nearly coincide (a focus bracket: sub-pixel shifts, up to a few percent of scale from focus breathing, much of every frame defocused). `photocraft_algo::direct` registers the target to the reference coarse-to-fine on a Gaussian pyramid of the luminance, the cost the mean-removed RMS of the warped difference over the pixels both cover, by a bounded Nelder-Mead search with a per-level schedule; the pose is a similarity about the centre, affine or projective per the projection, and resolution independent, so the search stops short of full resolution. `edit.autoAlignLayers` gets `registration`: auto (features, then intensity when nothing matches), features, intensity; layers register outward from the reference, each to the previous one aligned.
Closes#2064.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Intensity registration: reject a no-fit, search on coarsened planes
Review follow-up on #2065:
- `direct::cost` also returns the correlation of the reference with the
warped target over the overlap; `Registration.correlation` carries it.
Unlike the mean-removed RMS it is exposure independent, so it tells a
bracket (near 1) from unrelated pictures (near 0).
- `register_direct` fails with the existing "couldn't find enough matching
detail" error when a layer's best pose overlaps its neighbour by less than
a quarter of the plane or correlates below 0.5, instead of placing it at
whatever pose the search box ended at.
- The luminance planes are built at the search scale (`luma_alpha` with the
reduction factor, ≤ 2048 px across) and the warp of each registered layer
runs there too; the pose is resolution independent and its matrix at full
resolution places the layer. Nothing full-resolution is held.
- The reference index is checked once with `get`; no unclamped indexing.
- Tests: correlation at an exposure step, for unrelated textures and a flat
plane; the engine rejects unrelated layers and a mostly empty one.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Auto-Blend Stack Images: fuse the merged layer per pyramid level by region energy, with halo control
The merged layer of Edit › Auto-Blend Layers › Stack Images was the per-pixel masks blended with a multiresolution spline, so a wrong pick in the mask reached every scale. `pyramid::fuse_stack` fuses the layers on the Laplacian pyramid instead: per coefficient the layer with the largest region energy of its luminance band wins (Adelson et al. 1984's multifocus composite with Wang & Chang 2011's rules), the residual follows local deviation + entropy, and with halo control the levels coarser than the guide are weighed by the guide's energy so a bright object's defocused glow stays out. `haloControl` (0..8, default 2) on `edit.autoBlendLayers`; the masks and the panorama method are unchanged.
Closes#2043.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Auto-Align Layers: register focus and exposure brackets by intensity when there is nothing to match
Feature matching fails outright on layers that nearly coincide (a focus bracket: sub-pixel shifts, up to a few percent of scale from focus breathing, much of every frame defocused). `photocraft_algo::direct` registers the target to the reference coarse-to-fine on a Gaussian pyramid of the luminance, the cost the mean-removed RMS of the warped difference over the pixels both cover, by a bounded Nelder-Mead search with a per-level schedule; the pose is a similarity about the centre, affine or projective per the projection, and resolution independent, so the search stops short of full resolution. `edit.autoAlignLayers` gets `registration`: auto (features, then intensity when nothing matches), features, intensity; layers register outward from the reference, each to the previous one aligned.
Closes#2064.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Intensity registration: reject a no-fit, search on coarsened planes
Review follow-up on #2065:
- `direct::cost` also returns the correlation of the reference with the
warped target over the overlap; `Registration.correlation` carries it.
Unlike the mean-removed RMS it is exposure independent, so it tells a
bracket (near 1) from unrelated pictures (near 0).
- `register_direct` fails with the existing "couldn't find enough matching
detail" error when a layer's best pose overlaps its neighbour by less than
a quarter of the plane or correlates below 0.5, instead of placing it at
whatever pose the search box ended at.
- The luminance planes are built at the search scale (`luma_alpha` with the
reduction factor, ≤ 2048 px across) and the warp of each registered layer
runs there too; the pose is resolution independent and its matrix at full
resolution places the layer. Nothing full-resolution is held.
- The reference index is checked once with `get`; no unclamped indexing.
- Tests: correlation at an exposure step, for unrelated textures and a flat
plane; the engine rejects unrelated layers and a mostly empty one.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* Make large pixel operations allocation-fallible
* Warp: drop the fixed 400 MP cap; fallible reservations already cover large transforms
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Adapt #2530 to main: fallible steep-Distort warp, Result from mask paste in float moves; regenerate scorecard
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>
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>
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>