- swatches.import with bytes only (no path or format): an .act whose first
colour is 0, 1, x or 0, 2, x still went to the .aco reader, which ignores
trailing bytes and returned a handful of junk swatches. At the two .act
lengths (768/772) an .aco header now counts only when its sections span
the whole file (new `aco::sections_len`); a Photoshop .aco of exactly
768 bytes is still an .aco. Test: act_that_starts_like_an_aco_is_still_an_act.
- The .atn corpus tests (psd and engine) skipped unreadable folders and
entries silently; they now fail with the path.
- act::write doc: the trailer is written when `count` or `transparent` is given.
Co-authored-by: Abdul karim mia <97598943+abdul-karim-mia@users.noreply.github.com>
Co-authored-by: Fabio Lissi <fabio.lissi@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fill_to (Paint Bucket export): keep main's `source` alias and explicit
colour (`Usng` = `Clr ` plus `Clr `, #7199) together with the PR's
optional `Tlrn`/`AntA`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The warp places pixels within +-2^20 px (WARP_REACH) and clamps the rest
away. The byte budget admitted results wider than that (a 2,000,002 px
strip fits 8192 MB), and a plain move past it already emptied the layer
without an error. `warp_output_clipped` says when the result would be cut,
and the budget check refuses it: "would reach past +-1048576 px, the
farthest a transform places pixels". Docstrings for the touched functions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fixed 2^30 px cap refused any transform of a stitched layer past about
1 GP, on any machine. A result is now priced in bytes at its pixel format
(with the alpha the warp adds) against Preferences > Performance > Memory
Usage (default 8192 MB), which Session::apply_prefs sets. The report's
37708 x 35259 px 8-bit result (5072 MB) now transforms: 10.7 s, 6.1 GB on
top of the source. Past the allowance the message names it:
"... (10144 MB), more than the 8192 MB Memory Usage allows (Preferences >
Performance)".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Gradient Overlay (and gradient strokes, linked patterns, linked bevel
textures) is laid out over the layer's bounds. Painting grows those
bounds, but the stroke commands reported only the stroke as damage, and
the canvas's committed-stroke path repainted only what the live stroke
had not shown. The canvas therefore kept the old gradient around earlier
strokes: one gradient per stroke instead of one over the whole layer.
Session dispatch now grows a command's reported damage by
photocraft_compose::relayout_damage(before, after): the whole composite
of any visible layer whose effects span its frame, when that frame moved.
The canvas adds the same area to a committed live stroke. Strokes inside
the existing bounds and frame-independent effects keep tight damage.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Found while testing crates/algo/src/camera_raw.rs:592 (value taken unchecked in crates/engine/src/lens_cmds.rs:137 raw_params)
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
`compose::flatten` returns an empty buffer for canvases over MAX_RENDER_PIXELS.
Trim (Top Left) indexed `px[0]` of it and Duplicate › Merged Layers Only fed it
to `write_region`'s length assert: both panicked; Trim's other modes reported
"nothing to trim to". Both now return a "too large to flatten" error first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Group Layers now creates the group collapsed (#5571), so
shift_range_follows_visible_rows_on_both_sides_of_an_expanded_group expands it first, as its name
says. The line is from #5605 by @nrq6993-a11y.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A command that fails on a layer id or name the document doesn't have now says which layers there are, top-down as id and name (at most 20, names cut at 64 characters), so an agent can recover from the error alone: ids are not positions, and a freshly opened PNG's Background is layer 2. NoLayer becomes UnknownLayer { id, known } at both command entry points, keeping the 'no such layer' prefix. A layer name that matches no layer exactly matches regardless of case; an exact match always wins.
Fixes#5058.
A Hue/Saturation smart filter (Fltr class HStr, filterID 'HStr') opened
as psd.unsupportedFilter, so re-rendering the smart object (replace
contents, relink) dropped it, and a Hue/Saturation smart filter made in
PhotoCraft was left out of PSD saves.
The new hue_sat_filter_map module maps Photoshop's layout (measured on a
Photoshop-saved file: presetKind, Clrz, Adjs = master Hst2 {H, Strt,
Lght} plus one Hst2 per edited range with LclR, BgnR, BgnS, EndS, EndR,
all longs) to image.adjustments.hueSaturation with the command's params,
and writes it back in that layout. Anything outside it (unknown keys,
duplicate or unknown ranges, values the dialog can't hold) stays an
opaque unsupported filter written back verbatim. smart_map.rs only gets
the dispatch hooks.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A Perspective Warp smart filter (Fltr class perspectiveWarpTransform) fell
through to psd.unsupportedFilter, so a re-rendered smart object (e.g. after
Replace Contents) lost its perspective. Each quad now becomes a plane of
layer.smartObjects.perspectiveWarp (src = vertices, dst = warpedVertices),
and PSD save writes Photoshop's descriptor back: the original item with
only the geometry rewritten, or a new item in the layout Photoshop writes.
Inconsistent descriptors stay opaque, as before.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Tests: Free Transform applies when another tool, layer or New Layer is picked (#5665, #5667)
Whole-app regression tests for the paths #5665 and #5667 report (a moved box, and a placed image moved or untouched; toolbar tool click, Layers panel row click, New Layer). They pass on main, so these issues don't reproduce here; the tests pin Photoshop's behaviour (apply, never silently cancel) while the reporters narrow down their environment.
* New Layer while Free Transform is open applies the transform first (History: the transform, then New Layer; a placed layer's step no longer swallows New Layer); tests: a real placed fixture, the New Layer button, History order
* Free Transform + New Layer: check the command can run before applying the transform; a refused transform keeps the box and the placed layer and returns the refusal
* Free Transform + New Layer: Session::precheck validates the call (params shape, New Layer's values, precondition) before the box is applied
* Free Transform + New Layer: commit-first only for PRECHECKED commands; precheck shares dispatch's admission checks (named-layer precondition, jobs, hidden target) and checks a named layer exists; Group from Layers documents its layer param; test: precheck agrees with execute
* Free Transform + New Layer: precheck resolves a layer given by name as the command does; Layer via Copy/Cut leave PRECHECKED (their failures show only when they run); rebased (lib.rs mod lines)
* Shortcuts without the UI: shell commands and tools in the engine, reserved keys, set files
A retro against "everything is drivable headless; the UI is a layer" found the shortcut
work leaning on the UI. Fixed:
- The shell's command table (UI_COMMANDS: View › Zoom In, the panels…) moves into the
engine as data (shell_commands::SHELL_COMMANDS; menus::UI_COMMANDS is now an alias), with
the tools (shell_commands::TOOLS: id, Photoshop name, default key; a test keeps it equal
to the toolbar). edit.keyboardShortcuts binds, lists, imports, exports and checks them for
conflicts with no UI: a headless .kys import takes tool keys, and an export is the whole set.
Tools may share a key (Shift cycling); a command given a tool's key takes it from them.
- Reserved system keys are refused by the engine (the dialog uses the same check) and left
out of imported sets.
- importKys / exportKys / summarize take {"path": …} to read or write files, like
actions.save; the automation gate refuses those paths for MCP.
* Keyboard shortcuts: bindings commit only after their exports are written (a bad target changes nothing); docs: the command knows the tools
* Keyboard shortcuts: both export targets are resolved and checked before either file is written; a failed later write removes the earlier one
* docs: reserved keys per platform
* Keyboard shortcuts export: files are staged next to their targets and moved into place only when all are written; a failure leaves the user's files untouched
* Keyboard shortcut exports all or nothing: unique create_new staging and backups beside each target, restored on any failure; the same target twice refused; docs: Ctrl+Shift+Esc
* Shortcut exports: a failed backup move removes its placeholder; undo restores backups without deleting first and names any it couldn't; read-only targets refused up front
* Layers panel: blend mode, Opacity, Fill and locks change every selected layer; Photoshop's History names
With several layers selected, the Layers panel's blend mode, Opacity, Fill and lock controls
changed only the active layer, silently (the layers audit's top panel gap). They now change
every selected layer, as in Photoshop, in one undo step. layer.setProps takes
"layers": [id, …] for that (headless too); a list with an unknown id, a bad shape, or a
name for several layers is an error that changes nothing.
History names follow Photoshop for a single kind of change: Opacity Change, Fill Opacity
Change, Blending Change, Rename Layer (a mix stays Layer Properties).
* Layers panel with several selected: Pass Through only for groups (refused for other layers), the Basic lock toggle and the blend-mode hover preview take every selected layer, the controls grey with the Background selected; History icon for opacity/fill/blend changes
* Layers panel: leave the Background-in-a-selection behaviour as it was until Photoshop's is confirmed (#5856)
* Layers panel: a selected Background is left out of several-layer Opacity, Fill, blend and lock changes until Photoshop's behaviour is confirmed (#5856)
* Paste Layer Style carries Blend If and channels; align to an active selection with several layers
From the layers audit (L2):
- Copy / Paste Layer Style copied the effects, blend mode, Fill and advanced blending but
dropped Blend If and the channel restrictions; Photoshop's layer style carries all of its
Blending Options. Across colour modes only Blend If's Gray range carries over and the
channels reset (RGB channel ranges mean nothing in CMYK). The copied style is now a named
struct (CopiedStyle) instead of a tuple.
- With an active selection, aligning (Layer › Align, "auto") aligned two or more layers to
their own bounds; Photoshop aligns them to the selection (Layer › Align Layers to
Selection), as it already did for one layer.
* Paste Layer Style keeps Blend If normalised across modes; Align docs say it follows an active selection for any count; undo, group and 16-bit tests
* Clip groups: Release Clipping Mask takes the layers above; merges keep clipping; Delete Hidden settles
From the layers audit (L2, silently wrong results):
- Release Clipping Mask on a clipped layer also releases the clipped layers above it, as
Adobe documents ("removes the selected layer and any layers above it from the clipping
mask"). Before, they stayed clipped to the released layer.
- Merge Down of two clipped layers, and Merge Layers of clipped layers of one chain, give a
layer still clipped to their base (it was always unclipped). Merge Down's result also keeps
the lower layer's colour label and link; Merge Layers' keeps the top layer's label.
- Delete Hidden Layers releases the layers clipped to a deleted hidden base (clip_group rule
3, as single Delete already does), instead of leaving them clipped to the layer below.
* Merge Layers keeps the result clipped only when the clipped layers share one clip chain
* fmt
* TIFF: read and write CIE L*a*b* (CIELab, ICCLab); Lab documents stay Lab
Photoshop saves Lab documents as CIELab TIFFs (PhotometricInterpretation
8, a* and b* signed), which PhotoCraft could not open, and PhotoCraft
saved Lab documents as sRGB TIFFs.
- codecs: a Lab channel layout (Lab, LabA) holding a* and b* as
PhotoCraft's Lab documents do ((a + 128) / 255; 16 bits like the PSD
importer), with its own Lab <-> sRGB conversion (same formulas as
photocraft-color; codecs stays standalone) for formats without Lab,
which now warn LabConverted.
- TIFF read: CIELab and ICCLab (8 and 16 bits, alpha) are handed to the
tiff crate as RGB and labelled Lab; a* and b* are converted from the
file's encoding (signed a / 256a, or offset).
- TIFF write: Lab and LabA as CIELab, as Photoshop writes it; float Lab
is written with 16 bits (DepthReduced).
- io: a Lab TIFF opens as a Lab document; a Lab document with one plain
layer is written to TIFF in Lab (other formats still use sRGB).
Photoshop 2025's Lab TIFFs open with the colours Photoshop shows (its
own RGB conversion matches within one level at 8 bits and exactly at 16
bits), and Photoshop reads PhotoCraft's Lab TIFFs back with the same
L*a*b* values.
Fixes#4936
* PSD: a smart object's filter mask shows when the file opens
Photoshop stores a smart object's layer pixels with its filters applied
but not its filter mask. When it composites, it mixes the unfiltered
pixels (cached in FEid) back in through the mask. We showed the stored
pixels as they were, so a PSD whose smart filter has a mask opened with
the filter everywhere. Only after something re-rendered the smart object
did the mask appear, because our re-render mixes it.
- compose::filter_mask holds the mix, moved from the engine, and its
inverse.
- Import decodes the unfiltered pixels from FEid
(smart_map::unfiltered_from_item) and keeps the mixed result as the
smart object's pixels, as the engine's re-render does.
- Export writes the layer record the way Photoshop does: unmixed with
the unfiltered pixels it writes (or keeps) in FEid, so Photoshop mixes
back to what the document showed. Transparent pixels stay empty.
Photoshop oracles: sf-filter-mask-hard, sf-filter-mask-gradient and
sf-stack-blend-and-mask pass (145 -> 148 of 258); floor 145 -> 148.
psd-tools and io corpora unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* DNG: decode lossy (JPEG-compressed) DNG tiles (FILE-215-11)
DNG 1.4 lossy JPEG (compression 34892) stores each tile or strip as a
baseline-DCT JPEG of 8-bit samples: a gray stream for CFA data, a YCbCr
stream for three-sample LinearRaw. The decoder arm from #2021 read the
streams but widened every sample by 257, so the values no longer matched
the file's levels: an 8-bit WhiteLevel (or its default, 255) clipped
everything to white and a 256-entry LinearizationTable was indexed past
its end. It also required the JPEG frame to match the clipped row count,
which rejected the padded edge tiles real tiled files carry.
The arm is now `lossy_jpeg()` in sensor.rs. The decoded 8-bit values are
the stored samples, and BlackLevel, WhiteLevel (default 255 for 8-bit
data) and the LinearizationTable apply to them exactly as for every other
compression. A tile may be coded at the full tile size (edge tiles padded,
as the spec says) or at the image's row count; a strip at its row count;
the width must be the segment's. BitsPerSample must be 8 and the JPEG's
component count must equal SamplesPerPixel (1 or 3); anything else is an
error, never a silent conversion. The frame header is checked before any
pixel is decoded and the JPEG decoder's own size cap is the segment the
IFD declares (already charged against the limits), so a hostile SOF cannot
size a buffer past the tile.
testgen writes true 8-bit lossy fixtures (`LossyJpegTiles`, and strips via
the new `LossyJpegStrips`), accepts pre-encoded `lossy_segments` for
hostile-input tests, exposes `baseline_jpeg()`, and leaves WhiteLevel out
when `white` is 0. The DNG tests cover CFA and LinearRaw RGB from tiles
and strips at 37x23 (partial edge tiles, a short last strip), the default
white level, black/white scaling and a LinearizationTable against the
uncompressed path, and hostile inputs: a truncated tile, frame-size and
component-count mismatches, a 65535x65535 SOF under default and tight
limits, a tile over the allocation budget, and 12/16-bit or 4-sample
files. The hostile mutation set gains a lossy CFA and a lossy LinearRaw
file. The parity doc's DNG row is updated; the scorecard row is left for
the coordinating branch.
* 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.
* Vector: bound flattening memory, keep over-budget dashes, cheaper edge build (#5195)
- flatten: a path's flattened points are capped at 1 << 20; past it every
curve keeps a proportional share of its pieces (at least one), so hostile
handles from files (vmsk, SVG, effect outlines) cannot exhaust memory.
fill_rasterizer flattens the whole path under one budget.
- stroke: past the dash step budget (or below coordinate precision),
dash_polyline keeps the dashes made so far and draws the rest solid
instead of dropping the stroke. Open polylines are no longer copied.
- raster: coordinates are clamped to +-1e15 and edges with a non-finite
slope (vanishingly short) are skipped, so huge finite coordinates no
longer produce NaN coverage. fill_rasterizer sorts edges once instead of
after every component (stable sort, identical order).
- Cleanups: dead miter `half`, dead loop-length check in trace_mask,
shared mask render body, map_knots helper, shared straight-segment test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PSD: cap pattern memory across a parse, reject mixed-depth pattern channels (#5198)
- Patterns: one PatternBudget (decoded bytes + zero padding) is shared by
every pattern of a block, a .pat file and the pattern blocks of one
document; .abr patt sections are charged to the samples' budget.
- Patterns: a channel whose depth differs from the first is rejected (the
parsed pattern could not be written back).
- Slices: descriptor numbers no longer wrap; out-of-range ids are no id,
coordinates clamp to i32, and a v6 trailing entry without a usable id
attaches its outsets to no slice.
- LayerInfo::unpadded_len is computed from field sizes instead of
serializing every record into a scratch buffer.
- abr desc presets are moved out instead of cloned; UnFl and shmd counts
use the bounded preallocation; ImageData::decode_channel (decoded every
plane to return one, unused outside its test) is removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Vector: bound stroke outline points (#5211)
- stroke_polygons dashes all lines first, then counts round caps and
joins. When their full-resolution tessellation would exceed 1 << 20
outline points, every round cap and join gets the same smaller segment
count (down to 4 per circle, 1 per join; area-exact radius kept).
Strokes under the budget are unchanged; no piece is dropped.
- The dash step budget (100000) is now shared by all lines of a stroke
instead of per polyline, so many subpaths cannot multiply the dash
(and cap) count. Past it the rest is drawn solid, as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* 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: Gert Pellin <gert@pellin.be>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Fabio Lissi <fabio.lissi@gmail.com>
Co-authored-by: Vikram Bhaskaran <vikram.bhaskaran@outlook.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: Finn Newick <finnnk@gmail.com>
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>
* 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>
* 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: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Tanish Manku <tmanku.dev@gmail.com>
* fix(doc): variable definitions follow layer id remapping on copy and load
A duplicated document (image.duplicate, history.newDocument) or a .pcraft
loaded with remapped ids assigned fresh layer ids but cloned VariableDef.layer
unchanged, so datasets silently drove no layer (or, after remap, a colliding
one). Remap variable targets through the same id map as layer comps and
slices; a dangling target keeps a non-colliding placeholder. A merged-only
duplicate drops the whole variable binding set, matching how it drops comps.
Closes#4969
* style: cargo fmt duplicate_variables test
---------
Co-authored-by: highperfocused <highperfocused@pm.me>
* Crop tool: Content-Aware fill of the area a rotated or extended crop uncovers (#3823)
image.crop gains contentAware (bool, default false). After a turned crop or one that extends
past the canvas, the area the original image doesn't cover is filled from its surroundings on
the Background (else the active pixel layer), in the crop's one undo step. Each connected piece
(corner, edge strip) gets its own Content-Aware sampling window and samples only covered pixels;
the patch search is Edit > Fill > Content-Aware's (fill_cmds::content_aware_native, shared). A
crop that uncovers nothing is unchanged; one with nothing to sample from fails and rolls back.
Crop options bar: Fill: Background (Default) / Content-Aware Fill (ToolOptions
crop_content_aware, ui.set cropFill). Generative Expand is not offered.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Crop: adapt to Document::background after main removed is_background
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Crop: translate Background (Default) for Hungarian and Turkish
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Fix the merge with main: UI_SET_FIELDS length, Serbian Background (Default)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Merge main: UI_SET_FIELDS counts both new fields
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Smart Objects: inspect and Replace Contents show shared instances (#4075)
Placed layers that share a source have imported as one set of contents
since #2222, and Replace Contents changes every instance, but nothing
reported it: `linkGroup` is Link Layers, and `smartSource` can't tell
copies of one contents from independent copies of the same file.
document.inspect now gives each smart object `smartContents`: the
contents `id` its copies share and how many `instances` there are.
Replace Contents, Relink to File and Convert to Embedded/Linked list the
`instances` they changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Test rendered caches after shared Smart Object replacement
---------
Co-authored-by: jonathnwxh <258340642+jonathanwxh-cell@users.noreply.github.com>
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>
A mask made from the selection takes the selection with it in Photoshop: after
Layer > Layer Mask > Reveal Selection (the Layers panel's mask button with a
selection) the marching ants are gone. PhotoCraft kept the selection, so the
usual next step, Invert (Cmd/Ctrl+I) on the new mask, inverted only the part
inside the old selection and left the whole mask black, instead of flipping
the mask. Hide Selection (the Alt-click variant) already dropped it.
Reveal Selection now clears the selection in the same history step as the
mask: one undo brings the selection back and removes the mask.
Tests: adjust_cmds::tests::reveal_selection_drops_the_selection_so_invert_flips_the_whole_mask
(8/16/32-bit). Tests that deselected right after Reveal Selection no longer
need to (Deselect is disabled without a selection); layer_mask_depth and the
automation agent tasks now expect the selection to be gone.
Co-authored-by: abc2cb <271337822+abc2cb@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Session::execute catches a command's panic and returns an ordinary Err (the
last-resort guard in jobs.rs), so the unwind never reached panic_hunt's own
catch_unwind and a panicking command counted as a pass.
The harness now installs a panic hook that counts every panic in the process,
including on rayon workers, and a run fails if the count moved. Runs are
serialised so a panic is charged to the run that caused it. No production
code changes.
Test: contained_panics_count_and_graceful_errors_do_not. A contained panic
fails the run and a graceful Err passes; the first assertion fails without
the hook. The full --ignored fuzz stays green, so no command panics today.
Co-authored-by: prekabreki <prekabreki@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>