* Speed up Crystallize and Pointillize with cell-space rasterization
* Measure pixelate commands on unlocked layers and Background separately
* Record pixel-exact 24 MP filter and command benchmarks
* security(engine): bound script, batch, adjustment, import and export work from file input
Scripts calling scripts share depth and step budgets; batch inside a played action charges the playback tree; destructive adjustments and mask adjustments visit stored tiles when the content bounds are a huge sparse grid; Copy Layers checks group depth; aux flattening and brush import refuse oversized canvases; video footage frames are budgeted, Render Video names frames after the leaf of the document name and reads no linked files for restricted sessions; ATN names are clipped; profile paths are read as regular files only; prefs.set no longer treats colorSettingsX as colorSettings.
* style(engine): rustfmt
* engine: address review - refuse unbounded sparse adjustments, carry batch playback charges, charge played steps to scripts, clip ATN targets, stricter colorSettings paths, cap image-sequence frames
* skip redundant smart object resamples and per-pixel reads
* resample smart objects only when their re-render fails
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Crop: Delete Cropped Pixels keeps type and smart objects whole (#7116)
crop_doc cropped every surface for_each_surface visits, including the
rendered caches of type layers and smart objects. Those move by shifting
their stored pixels, so the cut stayed and showed as soon as they were
moved. Like Photoshop, Delete Cropped Pixels now deletes pixel-layer data
(and fill caches) only; shapes are re-rendered as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Address review: Delete Cropped Pixels crops stored video frames again (#4956)
The pixel-layer-only crop path skipped a video layer's stored frames and
original footage, so scrubbing brought the cropped pixels back. Crop them
with the displayed content, as before, and test a shrinking crop.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Address CodeRabbit review: the turned crop keeps type and smart objects whole before rotating
The pass that drops what a turned crop deletes before the rotation still cut
every cached surface. The rotation re-renders type and smart objects, but a
smart object whose source is unavailable keeps its cache, so it stayed clipped.
That pass now uses the same pixel-layer-only rule as the crop itself.
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>
* Paste: keep the pasted image on the canvas (#6759)
Paste centred the clipboard image on the view centre with no clamp, so an
image that fit the canvas could land partly off it, and one larger than
the canvas could leave blank edges. Now, per axis, an image that fits stays
wholly inside the canvas, as close to the view centre as it can, and one
larger than the canvas starts at the canvas's top-left edge, as in
Photoshop. Paste Into / Outside still centre on the selection
("clampToCanvas": false). Centres are computed edge by edge in f64, so
extreme coordinates no longer overflow.
* Paste: doc comments for Paste Into and the paste placement tests
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Auto-Align: a locked layer becomes the reference instead of being moved (#7080)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Address review: Auto-Align moves the Background as a normal layer
With the Background selected and a Cylindrical or Spherical result (every
layer is reprojected), or another layer as the reference, the Background's
own position lock made Auto-Align refuse. Like Free Transform and Photoshop,
the Background's built-in position lock no longer pins it: it becomes a
normal layer ("Layer 0") when it has to move. Lock All, Lock Pixels and a
user's Lock Position on any other layer still keep it as the reference.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Address CodeRabbit review: the Auto-Align params doc names the locked layer as the default reference
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>
`style.presets.new` with `includeBlending: false` and an explicit `opacity` stores only that
opacity, but `apply` with `add: true` skipped the blending block unless the preset had a blend
mode, so the layer kept its opacity. Test: an_opacity_only_style_added_sets_just_the_opacity
(also checks the layer's blend mode and Fill stay).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The .atn importer kept only bicubic, bilinear and nearest from a
transform's Intr, so a recorded Bicubic Automatic, Sharper or Smoother
Transform Selection replayed with the command's default (bilinear) and
re-exported without its choice. The bicubic variants are now kept (the
transforms already render them as Bicubic), Automatic becomes Bicubic
Automatic and Preserve Details Bicubic, and both transform commands list
the variants (#8697).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* fix(clipboard): render copied shapes in destination canvas space
When a copied shape group is placed beyond its source canvas, rebuild its vector caches against the destination canvas and pixel format. Previously a centered paste into a larger document could clip the caches to an empty source-sized frame, leaving no transform bounds and incorrect effects until a point edit refreshed each shape.
Restore missing shape caches and refresh source-clipped caches when canvas dimensions change. Cover Paste and Copy to Document, nested group contents, 8/16-bit destinations, rendered cache equality, transform bounds and movement Undo.
* fix(clipboard): measure shape placement before destination clipping
Restore missing source-space shape caches before measuring copy bounds, then adapt and translate for the destination. Re-render empty caches when shapes move back onto the canvas instead of shifting an empty raster.
Add focused integration coverage for 32 combinations of partial and full destination clipping, grouped shapes, missing caches, 8/16-bit output, centered and point placement, and Undo/Redo. Clippy and WASM checks pass.
---------
Co-authored-by: kotulsky <7805437+kotulsky@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
contained_panics_count_and_graceful_errors_do_not used the 4 s hang
budget meant for fuzzed commands. With RUST_BACKTRACE=1 the panic hook
prints a backtrace, and on loaded Windows runners the first symbol
lookup took long enough (19.6 s) that run_watched returned None instead
of Some(false). The self-test now waits up to 120 s; the fuzz keeps 4 s.
Photoshop records the composite entry of a Levels or Curves Set as
Chnl/Chnl/Cmps and silently leaves the adjustment unchanged when it is
Chnl/Ordn/Trgt, which is what the export wrote. Import already reads both.
Fixes#8543
* feat(guides): New Guide Layout takes lengths with units
view.newGuideLayout took pixels only. Its widths, gutters and margins now
also take text with a unit as typed in Photoshop's dialog ("1 cm",
"0.25in", "12 pt", "3pc", "10%"), at the document's resolution; percent
is of the target's width for columns, gutters and left/right margins and
of its height for rows and top/bottom margins, per artboard. The dialog's
Width and Height fields already pass such text through, so they accept
units now too.
Part of #4984
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(guides): New Guide Layout percent resolves per selected artboard
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(guides): assert the full guide lists after a selected-artboard layout
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(actions): a recorded Layers-panel drag keeps its drop target by name
Dragging a layer while recording stored layer.moveTo's target as a layer
id, which means nothing in the document the action later plays in, so
playback stopped with "no such layer". The target is now recorded by
name (with targetOccurrence among same-named layers), as layer.select
records its layer, and layer.moveTo accepts a target by name.
Fixes#7845
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(actions): count a drag's same-named targets without the moved layer
Steps are recorded after they run, so a target's occurrence among
same-named layers could shift when the moved layer shares its name. It
is now counted among the other layers only, at recording and at replay,
which the move doesn't reorder. Only a drag of the selected layer is
recorded by name; drags of several layers, or of an unselected one, keep
their ids.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(actions): a moved group's contents don't count as drop targets
Same-named layers inside a moved group also changed order with the
move, so they are left out of the occurrence count with the group.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(actions): leave every moved layer out of a named drop target
A move of several layers given a target by name no longer counts the
moved layers (or their contents) as candidates.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(actions): a recorded ⌥-drag copy beside itself replays by name
A copy leaves its source in place, so the source stays a candidate for
the named target, at recording and at replay.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(actions): a recorded copy's own contents don't count as drop targets
The step is recorded after the copy exists; the copy (and, for a group,
its same-named contents) is left out of the occurrence count, as at
replay it doesn't exist yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(engine): refuse partial Color Balance and Hue/Saturation ATN exports
A `layer.setAdjustment` step changes only the settings it names, but the
Color Balance and Hue/Saturation descriptors written to the ATN carry
every setting and filled absent ones with defaults, so a partial edit
replayed as one that also reset the rest. Strict export now refuses such
a step (with skipUnsupported it is listed in Skipped); complete ones
export and round-trip as before.
Fixes#8531
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(engine): refuse Hue/Saturation ranges missing their own settings
A colour range written without its own hue, saturation and lightness
would replay with the missing ones reset to 0, so strict export refuses
it like a partial master setting.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(engine): refuse wrong-shaped Color Balance and Hue/Saturation settings
Strict export now checks the triplets, numbers and flags it needs are of
the shape the descriptor writer reads, since it would write anything else
as 0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(engine): refuse malformed Hue/Saturation range bounds
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(engine): cover malformed and valid Hue/Saturation range bounds
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(engine): refuse fractional Hue/Saturation range bounds
The descriptor stores them as integers, so a fractional bound would be
rounded on export.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(type): type.edit with new text keeps the styles of unchanged text
`type.edit {"text"}` replaced the whole string, so every character took
the first run's style and other styled ranges were lost. It now replaces
only the span that differs (after the common prefix and suffix), so the
unchanged text keeps its character and paragraph styles and an added
character takes the style beside it, as the params doc promised.
Fixes#8489
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(type): text inserted at a paragraph's start joins that paragraph
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The generated droplet shim only needs to run as its creator, so 0700 keeps
other local accounts from reading the CLI path it invokes. The reversed
range in the arrow-key test is the case under test, not an accident.
🤖 Generated with Codebuff
Co-authored-by: Codebuff <noreply@codebuff.com>
refine_mask_with builds its result densely over the selection's bounds
(grown by the spread, plus Refine Edge Brush additions). After a thin
edit across a huge canvas, or with a whole-canvas selection there, that
is billions of pixels. matting::refine_area names that area and
select.refineEdge refuses one over MAX_REFINE_AREA (2^30 px) with an
error before allocating, leaving the selection unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FszacAJDjznVimaxQktoJT
The Brush edit, the Lasso edit and EdgeBrush::new allocated a dense buffer
over their bounding box, which the swept-area cap does not bound: a 1 px
diagonal across a 100,000 px canvas sweeps ~141k px but asked for ~40 GB.
- matting::brush_surface (replaces brush_region) and EdgeBrush paint only
the tiles a stroke touches (painted_tiles), capped at MAX_PAINTED_TILES.
- selection::polygon_surface rasterises only the tiles the Lasso's edges
cross, byte-identical to selection::polygon; interior tiles share one.
- selection::combine_sparse adds/subtracts visiting only the new shape's
tiles, keeping the rest of the selection shared.
- select.refineEdge refuses edits and refineStrokes that would cross too
many tiles before painting anything, leaving the selection unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FszacAJDjznVimaxQktoJT
The tilt-shift reference tests and tiled_clear_and_fill_match_one_pass_over_the_area cross pixel
formats or channel counts with independent geometry and fill settings. Each run takes a quarter of
the combinations through Sweep, a different quarter each UTC day.
precheck_agrees_with_execute built all five document states for every call and kept one; it now
builds only the state the call needs.
Review of #8783:
- The Invert edit no longer builds a canvas-sized buffer: the canvas
takes the inverse of the selection's untouched value (one shared tile
for every fully covered tile, as fill_rect stores it), then only the
tiles the selection holds are inverted. Byte-for-byte equal to the
dense inversion; a 100k x 100k canvas inverts in well under a second.
- A run of Inverts is applied as one or none (pairs cancel exactly), so
4096 of them cost at most one pass between other edits.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of #8783:
- matting::brush `segments` no longer collects its pieces: they are made
as they are painted, and each segment is first cut to the painted area
grown by the brush. A 1 px stroke zigzagging across 2 million px
(within MAX_SWEPT_AREA) split into ~134 million pieces collected up
front (~2 GB) in brush_region and EdgeBrush::new; now ~5000 pieces.
- select.refineEdge `edits`: the Brush edits' swept area in all is
bounded by MAX_SWEPT_AREA, as refineStrokes' is.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
select.refineEdge takes `edits`: the Select and Mask workspace's Quick
Selection, Brush, Lasso, Object Selection, Select Subject, Clear Selection
and Invert, applied in order to the starting selection (or layer mask)
before the edge is refined. Cancel discards them, OK is one history step,
and agents drive them over MCP and the control channel.
- engine/select_mask_edits.rs: parse (bounded, every bad field an Err)
and a pure apply/apply_one the workspace preview can share
- algo matting::brush_region: soft round brush coverage (hardness)
- ATN export reports steps with edits instead of dropping them
- "Edits" label in the 21 catalogs
Part of #7069 and #8148 (the workspace UI follows in a second PR).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#5598 made File > Generate > Image Assets require Preferences > Plug-ins >
Generator, so #8254's two artboard tests got Disabled. Enable it first,
as #5598's own test does.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
After merging main, the Layer Style dialog always sends From/To as 8-bit
hex (#7072). On a baked Gradient Overlay without a source record (PSD or
.asl import) the custom and no-record paths wrote those hex ends back,
so OK without a change rounded the curve's ends (a Smooth bake's white
end 0.99999994 became 1.0, Lab ends became RGB). Like edit_endpoint for
the recorded source, a hex end that only repeats the current end is now
left out. baked_overlay_is_unchanged_until_method_selection_then_edits_
the_whole_curve fails without this.
Co-authored-by: oRE-o <42003350+oRE-o@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>