Merge 655295b2a2. Resolve generated scorecard conflict with exact upstream text only; actual source-bound regeneration and combined validation remain pending.
Keep command-local history, purge and rollback intact, then consolidate affected documents at action completion or dialog suspension. Preserve semantic no-op identity and redo, visibility semantics and row identifiers. Bind dialog continuation to its exact document/history, inherit the setting in scratch sessions and carry droplet action names. Add the review counterexamples and focused continuation, Batch/droplet and history-option controls. Source is formatted; compilation and runtime qualification remain pending.
One unreadable tile used to block editing, saving and exporting every open document
(spill::check_integrity was process-global). Add Tile/Surface/Document::scratch_health
(free while no tile has failed) and Document::for_each_surface (ops' history walk now
delegates to it). edit_with checks the document before and after, jobs the document they
captured, import the result, save/export the document written; check_since is now
generation-only so a failure or recovery during an operation refuses it once and a retry
works. Caches check their own sources and purge once per new failure instead of every
frame. Fault-injection tests use a duplicated handle (the Unix file has no name).
Resolve conflicts with main's prefs UI split, session/execute.rs move, GPU memory
budget (gpuMemoryMb, 0 = auto, kept from main) and effective_memory_usage_mb history
budget. Port the Memory Usage % and Scratch Disks preferences into prefs_ui, the edit
integrity guard into session/execute.rs, and adapt main's new pixel readers to the
guarded TileBytes API. Cargo.lock regenerated; scorecard regenerated.
History counted a document's pixel tiles but not the file bytes a
smart object embeds (SmartSource::Embedded). A document of large
embedded sources was far over its memory budget without trimming
ever noticing, and every Replace Contents kept the old bytes alive in
the undo states unaccounted.
The four counters (add, remove, bytes_with and the uncounted walk)
now visit one shared list of blocks: the tiles as before plus each
embedded source's Arc, keyed by address. States, snapshots and
duplicated layers that share an Arc count it once, and trimming the
last state that holds an old source releases it.
Main now carries #9031 (Halftone) and #8923; this branch keeps only what it adds on top: the guard for a setting beside a Gallery stack's effect list, its test, and the PSD open warning for patterns that paint nothing.
Main reorganised the source tree (command modules under engine/src/cmd, tools and dialogs under ui-egui/src, large files split into modules, inline test modules moved to their own files). This branch's changes to the moved and split files are ported to their new locations.
Clear History moves the History Brush source into the cleared states'
record and leaves History::brush_source empty. While Clear History can
be undone, trim_with trims the cleared states, but it looked the
pinned source up in the emptied field. The source state was then
dropped like any other old state, and after Edit > Undo Clear History
the History Brush had no source.
trim_with now takes the source from Cleared::brush_source while Clear
History can be undone, and from History::brush_source otherwise, as
History::held picks the cleared or the live states.
Keeps main's Clear History, state ids and History Brush source (#3534, #3536 and
the integration fix-up) together with History Options: states carry both their
id and their visibility; non-linear record keeps the pinned brush source; trim_with
keeps the source-first logic. The rollback log now records the index each state
was dropped at (the brush source is never dropped, so it is not always the front)
and the replaced document, so a rollback puts every state back where it was.
Clear History ends the rollback window. Catalogs: one line per string.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In non-linear history a record moves the later states onto the undo side and trims to the
History States limit (and the byte budget) at once. A compound command whose later part failed
then could not take its first step back when the trim dropped the state it replaced: the
fallback Undo landed on the last later state and cleared the rest. Even when that state survived,
the oldest states were lost. The latest record now holds the states it evicted until the next
change to the list, and History::rollback puts them back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With Make Layer Visibility Changes Undoable off, stepping through history kept every layer's
current visibility, so undoing a recorded step that changed visibility (Apply Layer Comp, a
snapshot, a compound) left the new visibility in place. Each history state now remembers the
layers whose visibility its own step changed (History::current_visibility, set by the session
when it records); Undo, Redo, History rows and non-linear Delete carry only the other layers'
visibility, so only the unrecorded eye changes survive. Deleting, purging or branching keeps the
lists consistent by merging them into the following step.
history.deleteState is now enabled in non-linear history while the oldest state is current and
later states exist, so a later state can still be deleted there.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- With Make Layer Visibility Changes Undoable off, Undo, Redo, a History row and
non-linear Delete keep every layer's visibility as the user left it.
- History::rollback takes the last record back exactly (also the later states it
moved in non-linear history); Transform Again on a copy, New Frame from Layers,
the Free Transform take-back and an abandoned Alt-drag copy use it via
Session::rollback. The Alt-drag fold keeps the moved later states.
- Non-linear Delete reaches a later (dimmed) state; the History panel lets those
rows be dragged onto the trash.
- Reword the snapshot-option comments: they come with #3634.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Edit > Purge > Histories resets a state source to the default instead of
leaving every document without one (History::clear).
- The pinned source state counts against the history byte budget first; the
newest states fill what remains, so trimming stays within max_bytes.
- The History Brush and Fill > History refuse a source of another canvas size
(after Crop, Canvas Size or Image Size) instead of painting shifted pixels.
- Snapshots can be the source: history.setSource {snapshot}, a source column on
snapshot rows, HistorySource::State|Snapshot in the engine; deleting the
snapshot leaves no source, Purge > Histories keeps it.
- paint.historyBrush and Fill > History take History panel rows for `state`
(redo rows included), as history.setSource does, and accept `snapshot`.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Step Backward in the History panel puts a floating selection back like Edit > Undo; Step
Forward and Edit > Redo put it back and step instead of dropping it first (which recorded a
Move and wiped the redo states); with nothing to step to the piece keeps floating.
- Memory figures and the byte budget count the states Clear History keeps for its undo.
- Undoing Clear History keeps a saved document clean.
- New Snapshot... in the panel menu runs history.newSnapshot.
- Tests: Clear on one document leaves the others alone, and each fix above.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Raw: measure the Nikon D90 image area (4288x2848 at (10, 10))
Aligning the decoded D90 sensor plane with the camera's own JPEG puts the
image at (10, 10) in the 4352x2868 sensor plane, giving the camera's
4288x2848 frame (10-pixel margins; the right border reads the camera's flat
255 from column 4310). Add the measured ImageArea so a D90 NEF opens on the
camera's frame instead of the uncropped plane, and make a measured profile
leave a file that carries its own colour calibration (a DNG) untouched.
* Codecs: open Cineon (.cin) film frames read-only
Cineon is the 10-bit logarithmic film-scan format DPX grew from. Read it from
the same Kodak header family: the generic header (big-endian, orientation at
192, channel count at 193, the channel-1 bits/dimensions), one or three
channels, three 10-bit samples packed into each 32-bit word on a word
boundary. The 10-bit code values open as U16 (round(c x 65535 / 1023)); the
file's log black/white points are a display choice and are not applied. A
depth other than 10 bits or a channel count other than 1/3 is Unsupported;
truncation errors rather than panicking. Read-only (no Cineon writer), so
.cin joins the open filters and Cineon read closes part of #1049's G10 gap.
* PSD: write tiff blocks without copies, inflate merged ZIP once
- ImageSourceData::to_bytes writes the global mask and global blocks
straight from the model with 4-byte alignment instead of cloning each
block (data included) to set its padding. Same bytes.
- The tiff transcoder's nested regions and blocks write into the parent's
buffer (taken and handed back, error or not) instead of building their
own output and copying it up per nesting level. Bodies that fail or are
not transcoded are truncated before being copied as stored or dropped.
Same bytes.
- PsdFile::from_bytes_and_merged decodes the merged image with the parse
when asked: a ZIP stream is inflated once (the decode's stream check is
the parse's); the parse-only check runs only when the decode fails, to
keep from_bytes' errors. PSD import uses it with merged_needed.
- PsdBuilder::composite documents that Gray/CMYK composites stay straight.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Content-Aware Fill: measure a thin hole by its thickness, not its bounds (#1366)
Removing long, thin objects (coiled guitar strings, wires) left their
outline as darker and lighter blotches and copied pieces of other strings
into the fill. The pyramid shrank until the hole's bounding box was four
patches across: for a string across the image that is a level where the
hole has swallowed its surroundings, so the matches found there copy
whatever looks like the blur, and the finer levels (refine-only for wide
bounds) keep them.
A hole at most a quarter as thick as its bounds are long now counts as
thin: the pyramid stops while it is still about two patches thick, and
that level starts from a membrane across it (over a few pixels, the
surroundings themselves) instead of the onion peel, whose exhaustive
search is what made a shallow pyramid unaffordable. Compact holes are
unchanged bit for bit.
Refs #1366
* perf(compose,gpu): effect caches reserve tile addresses instead of pinning layers
Effect map cache entries kept a clone of their layer (a deep clone for groups) so the tile addresses the cache key hashes couldn't be reused. That kept an edited or deleted layer's pixels alive, uncounted, until the entry was evicted. Keep a Weak to each hashed tile instead: it reserves the address without keeping the pixels. The GPU compositor's styled-group pins and shape-source tile snapshots do the same. Part of #4530.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(history): count snapshot-only tiles against the history budget
History panel snapshots outlived every budget: a Merged Layers snapshot is a full flattened copy that neither the history budget, the GPU budget's document bytes nor Purge accounted for. Trim now counts the tiles only snapshots hold as fixed cost, the GPU budget includes them, and Purge > Histories no longer reports tiles a kept snapshot still holds as freed. Part of #4530.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Content-Aware Fill: every part of a thin selection keeps its surroundings (#1366)
Two more causes of ghosts when removing strings and the small knobs
selected with them:
* Auto sampling summarises the selection's surroundings by a few
clusters. Around a long selection those come from the strings'
surroundings, so a small part elsewhere (a tuning knob in a differently
coloured corner) lost its own surroundings and was filled from far away
in another colour. Around a thin selection the Auto area now always
reaches as far as the selection is thick.
* The membrane that starts a thin hole now starts a patch radius out, past
the anti-aliased fringe or halo that a selection drawn close to the
object leaves outside it.
Refs #1366
* Content-Aware Fill: peel a thin hole where that stays affordable (#1366)
Starting every thin hole from a membrane smoothed Spot Healing on film
grain (spot_healing_keeps_film_grain_through_overlapping_strokes: grain
9.0 against 12.1 around). The onion peel copies the grain; it was only
given up because its exhaustive search is too slow on the big level a
long thin hole stops at. It now starts the thin hole whenever hole cells
times sources stay within 2^28 comparisons, and the membrane only beyond.
Refs #1366
* Select Subject: keep subject parts coloured like the backdrop, leave enclosed gaps open
Users on #2788 reported Select Subject dropping clothes that are close in
colour to a plain wall and bridging the gap between an arm and the torso.
Both come from the same place: the saliency pass labels low-contrast parts
of the subject as background, the background colour model then learns
them and explains them away, and the final blind hole fill closes every
enclosed gap up to 5 % of the mask.
Two changes in crates/algo:
- A second GrabCut pass whose background model only knows the backdrop. A
colour model fitted to the image frame (minus a margin around the
salient blob, in case the subject runs out of the picture) marks every
pixel it explains as probable background; the blob and whatever the
backdrop cannot account for start as probable foreground. Only
components that touch the first pass's result survive.
- Hole filling that consults the colour models: specks (<= 0.2 % of the
mask) always fill, larger holes up to 5 % fill only when their pixels
fit the foreground model better than the background's.
Tests: two regressions (a cream-shouldered figure on a near-white wall
keeps its shoulders; a ring keeps its backdrop-coloured hole open), plus
ignored timing and picture-dump helpers. The 6 MP first run goes from
about 100 ms to about 200 ms on one core; the P29 repeat path is untouched.
* Keyboard shortcut sets: Set menu, New Set, Save, Delete, .kys Export and Summarize (K2)
Edit › Keyboard Shortcuts gets Photoshop's set management. A set is a saved copy of the
bindings (Preferences.shortcutSets, the one in use in shortcutSet; "" is PhotoCraft Defaults),
and the Set menu shows "(modified)" when the bindings differ from it. New Set…, Save and
Delete wait for OK like the rest of the dialog. Export… writes the dialog's bindings as a .kys
file (commands then tools, Photoshop's macOS key spelling), each row carrying our id as pc-id
so PhotoCraft imports its own sets back exactly, whatever the label; Summarize… writes
Photoshop's HTML list of every bound command by menu.
edit.keyboardShortcuts takes useSet (before the call's edits), saveSet, deleteSet, sets and
activeSet (after them), exportKys and summarize; every reply reports the sets, the set in use
and whether it is modified. Bad names, keys or counts are errors that change nothing.
* .kys import: tool keys and sets saved on Windows (K3)
Photoshop's <tool> rows now import: each matches our tool by name (our tool names are
Photoshop's) or by pc-id in sets PhotoCraft exported, and a letter other than the tool's own
becomes its override, while its own letter keeps the tool cycling with the others (#5036).
A set saved on Windows spells the primary modifier Ctrl where a Mac set writes Cmd and keeps
Control for the Control key. A set with no Cmd or Opt anywhere but some Ctrl is read as a
Windows set, its Ctrl as our Cmd: Ctrl+L is the Cmd+L default, not a Control-key override that
would also mean Control on a Mac. The import reports `windows`.
* cargo fmt the extension lists
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Vikram Bhaskaran <vikram.bhaskaran@outlook.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Gert Pellin <gert@pellin.be>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Diego Chagas <drochachagas@gmail.com>
Co-authored-by: Tanish Manku <tmanku.dev@gmail.com>
A pattern fill or layer style copies its pattern into the document, but
the tile visitor behind History's byte budget skipped Document::patterns.
The pattern's pixels were left out of the budget, of trimming and of
Edit > Purge > Histories, including after an undo left them only to the
redo states.
Related to #4972.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* ops: count selection masks in history pixel budget
* test: expect unclipped HDR levels in float documents
* web: optimize engine code for the wasm size budget
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
History panel menu > History Options... in Photoshop has five options; this lands the two that
don't need snapshots, as preferences (`historyOptions.*`, `prefs.set`, reported by `ui.inspect`
under session.historyOptions) plus the dialog and a `history.options` command.
- Allow Non-Linear History (default off): an edit after stepping back keeps the later states and
is appended at the end (ops::History::non_linear); History panel Delete then removes only that
state (History::remove / drop_current), the later ones stay.
- Make Layer Visibility Changes Undoable (default on, as in Photoshop and PhotoCraft today): when
off, the eye, Alt-click (Show Only), Hide/Show Layers change the document without a history
state (Session::edit_visibility), leaving redo states alone.
- history.options: with params sets them (validated, unknown keys and non-bools are errors);
without params the GUI opens the History Options dialog (checkboxes, OK/Cancel).
- New strings translated in all 16 catalogs.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The History panel's menu (☰) now has Photoshop's items: Step Forward, Step
Backward, New Snapshot… (greyed, snapshots aren't built yet), Delete, Clear
History, New Document, History Options… (greyed, built in parallel) and Close,
above the dock's own group items. Each runs an engine command.
- history.stepForward / history.stepBackward: one state along the list
(Err when there is none).
- history.clear: removes every state but the current one without changing
the image. It is undoable: History keeps the cleared states until the next
step, and Edit > Undo puts them back. Fade is dropped and can no longer
underflow on an empty history.
- history.deleteState is enabled only when there is an earlier state.
- Translations for the new labels in all complete-menu languages.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The History panel gets Photoshop's source column at the left of every state:
the History Brush icon marks the source, and a click there makes that state
the source through the new `history.setSource {state}` command (not a step,
and the row is not stepped to). `paint.historyBrush`, the interactive History
Brush and Fill > History use the source when no `state` is passed; by
default it is the oldest state held, as before.
The source is kept by state, not index: photocraft-ops gives every history
state a stable id, so the source stays on its state while older states fall
off. As in Photoshop (checked by script), the source state itself does not
fall off the history-state limit or the memory budget, and once it is
deleted or discarded by a new step after stepping back there is no source:
the History Brush then refuses to paint without an explicit `state` until a
new source is set. Rows count the greyed redo states too, so the common
"filter, step back, paint the filtered state in" workflow works.
`document.inspect` / `ui_inspect` report `historySource` {state, label}
(null when its state was deleted). Snapshots are not implemented yet:
`snapshot` returns an "unsupported" error.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every edit's History::trim rebuilt a set of every tile in every retained state, even well
under the budget. Retained tile references are now counted as states are recorded, evicted,
undone, redone, purged and cleared, and below the budget trim only looks at the current
document's tiles. Record plus trim on a 24 MP, 150-layer document with 50 states drops from
62 ms to under 2 ms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>