#2006 made a bevel size change dearer on purpose (+10-75% on the GPU canvas,
2x for CPU text bevels) and asked for a bench of it under perf/.
`bevel_bench` renders a 12 MP document with one Chisel Hard inner bevel on
an anti-aliased ellipse (a smooth outline) or on a text paragraph (many thin
edges). Each case warms the caches one pixel below the size, then times
refreshes with the size growing by one pixel each, so the GPU canvas
rebuilds its distance fields every time (it keeps them while the size
shrinks) and the CPU compositor misses its map cache. Sizes 8, 50 and 250,
GPU and CPU, with `--scene`, `--size`, `--reps`, `--no-cpu` and `--json`.
Registered in perf/budgets.toml as P52-P59 with no budgets (track only,
like P45 and P46): GPU for ellipse and text at 8, 50 and 250, CPU at 8.
docs/scorecard.md is regenerated; the rows read "not run" until the next
baseline.
RTX 4080 SUPER, release, 9 reps, p50 ms, before #2006 (3bc49f5e) -> after:
ellipse GPU 88 -> 105 (8), 169 -> 199 (50), 305 -> 460 (250); text GPU
119 -> 140, 141 -> 252, 238 -> 557; CPU text at 8 402 -> 797. Runs vary by
about 20% on this machine.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Import Affinity issue 1606 special shapes
* Affinity corpus: give the #1606 sample ceilings a margin for font differences between machines
The samples' labels render with whatever fonts are installed, so the measured
differences move by up to 0.8 between machines; each ceiling is now the higher
measurement plus about 1. Also use as_chunks in on_white (clippy with --features corpus).
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>
* feat(canvas): Caps Lock forces the precise crosshair for painting tools (#1758)
* Caps Lock poll: back off for 5 s after an X error instead of reconnecting and warning every frame
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>
In a Grayscale or CMYK document, effect colours (overlays, shadows, glows,
strokes, satin, bevel, gradient stops) were stored as RGB and drawn
unconverted: a red Color Overlay in a Gray document was red. Effect::in_mode
converts them where effects are written: the effect commands, the Layer
Style dialog (replace), Paste Layer Style and style presets.
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* DNG: JPEG XL compression 52546 (DNG 1.7, Samsung Expert RAW)
Decode JPEG XL-compressed DNG strips and tiles with jxl-oxide (pinned
=0.12.6), with geometry and channel checks, Limits/AllocTracker budgets
and catch_unwind around the decoder. Warn when a DNG carries a
ProfileGainTableMap (tag 52525), which is not applied yet.
Tests encode fixtures in-process with zune-jpegxl (dev-dependency only).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* raw README: list Deflate DNG beside JPEG XL after rebasing onto #1971
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>
In Photoshop a single click (no drag) with a shape tool opens a creation
dialog to type the exact size. PhotoCraft ignored the click: the drag
geometry was below 1 px, so finish_shape returned without doing anything.
- New shape_dialog module: Create Rectangle (width, height, four corner
radii with a link toggle, From Center), Create Ellipse, Create Triangle
and Create Polygon (sides, star ratio). Width and height show in the
ruler unit and are stored in document pixels; values are remembered per
tool. OK runs the existing shape.create command at the clicked point
(or centred on it) with the options bar's fill and stroke; invalid
sizes/positions are rejected with an error, never a panic.
- canvas::finish_gesture opens the dialog when the gesture never left
the press point by more than 3 screen px; drags draw exactly as before.
Line and Custom Shape keep ignoring clicks.
- vector_ui::create_shape shares the fill/stroke params with the drag path.
- Catalog entries for the new labels and titles in every complete language.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
An RGB pattern (every built-in one) showed in colour as a pattern fill or
Pattern Overlay in a Grayscale or CMYK document, unlike Edit > Fill, the
export, and Photoshop, which fills in grey but keeps the pattern's colours
(a Grayscale round trip brings them back).
pattern::pattern_rgba projects a pattern's pixels through the document's
mode when painted; the CPU prepared tiles and the GPU pattern textures use
it. Patterns themselves are unchanged.
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The Crop options bar gets Photoshop's gear menu (Set additional Crop options): Show Cropped Area
(H toggles it while the Crop tool shows a box; off hides everything outside the box behind the
canvas colour) and Enable Crop Shield with Color (Match Canvas, the pasteboard colour, or Custom…,
which opens the Color Picker), Opacity (default 75 %) and Auto Adjust Opacity (half the opacity
while the box is dragged). The shield used to be a fixed black at about 51 %. The options persist in
ToolOptions.crop_shield (older settings get Photoshop's defaults; a stray opacity is clamped) and
ui.set takes cropShield. Classic Mode and Auto Center Preview are left out: PhotoCraft always crops
in Classic Mode.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Crop options bar gets Photoshop's Straighten button (one-shot), and holding ⌘/Ctrl with the
Crop tool is a temporary Straighten. The drawn line (with an angle readout) turns the frame so the
line ends up level, or plumb when it is nearer vertical (the Ruler's straighten_angle convention),
and fits the frame to the largest of its proportions inside the image (Classic Mode). It is a
pending frame edit: Enter commits, Esc cancels. New logic lives in crop_straighten.rs.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Changing the moving layer's opacity, fill or visibility in the Layers panel
during Free Transform was applied and kept on commit, but the preview showed
the session's starting state until then: the moving pixels kept their first
opacity and the document under them was the copy taken when the box opened.
follow_edits rebuilds that copy from the current document whenever its
revision changes (keeping the lifted pixels out) and takes the tint from the
layer's opacity x fill, or 0 when hidden. canvas::display_doc calls it before
reading the preview, so the canvas never caches a new revision with the old
preview. Lone mask/channel transforms and Transform Selection are unchanged.
Blend modes are still not shown on the moving pixels (an egui mesh blends
normally); that needs the compositor and is left for discussion in #1384.
* EXR: multi-part files open the colour part; exr_info lists them all
A Maya/Arnold render writes one part per AOV. The decoder took part 0
silently, so a depth-only part failed the whole file with "no R/G/B or
Y channels". Now the part that looks most like a colour image opens
(RGBA > RGB > YA > a single channel, earliest wins), with a
DecodeWarning::MoreParts naming how many parts exist.
exr_info lists every part (name, view, size, channels, deep/tiled) from
the headers alone, and decode_exr_part decodes a chosen part — F16 parts
stay F16. Deep parts stay with the deep decoder: a file that has any is
refused here with a pointer to decode_deep_exr, by design.
* EXR: cap the parts' sizes together, move the chosen part out instead of cloning it
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>
The Layers panel "Add a mask" button always dispatched
layer.layerMask.revealAll/hideAll/…Selection, which replace the layer's
mask unconditionally. A second click (Alt or not) silently discarded a
painted mask; Alt-click only looked like "invert" because hide-all
replaced reveal-all.
Like Photoshop, the button now adds a layer mask when the layer has
none, a vector mask (Alt: Hide All) when it already has a layer mask,
and is greyed when the layer has both (or is a shape layer with a layer
mask). Its tooltip becomes "Add vector mask" once a layer mask exists.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Brush phase 3a (owner-approved brush overhaul):
- Every stroke point now carries a time (`timeMs`). egui's pointer events have none, so a frame's
samples are spread evenly between the previous point and the frame time. Speed spacing, airbrush
Build-up and Stroke Catch-Up worked from scripts but never from the canvas, because every point
had time 0. The live stroke and the commit get the same times.
- A stroke started on the press takes its moves from the press on. The moves before egui
recognised a drag (its 6 pt click distance) were dropped, which gave every stroke a straight
start and lost the pen's pressure ramp-in.
- Smoothing pulls by stroke time (16 ms steps, fractional below that), not once per input event,
so a 1000 Hz mouse and a 60 Hz pen feel the same. Without Stroke Catch-Up a paused pointer stops
the brush, as in Photoshop.
- A pointer held still keeps the stroke's clock running only while the stroke changes with time
(airbrush Build-up, or smoothing still catching up): it repeats the held point every 16 ms and
asks for repaints, then goes idle.
* Layer styles: chiselled bevel facets are flat at any edge angle (#1926)
Chisel Hard and Soft shaded the facets of an edge at 30, 60, 120 or 150
degrees in regular cross-stripes (a comb along the edge), while 0, 45, 90
and 135 degree edges were clean. The height of a chisel is a linear ramp of
the distance to the edge, and that distance was measured to the nearest
pixel centre of a thresholded mask: an oblique edge is a pixel staircase,
so the distance ripples with its period (0.19 px at 60 degrees), and the
shading is a gradient of it, scaled by depth.
Chisel bevels now measure to the edge itself. A sharp anti-aliased edge
pixel (partly covered, touching an empty and a full pixel) places the edge
by its coverage along its own normal (edge_offset inverts the area a
straight edge cuts off a pixel), and a pixel measures to the line of the
edge pixels around the foot of its perpendicular, not to the centre of the
nearest one (edge_distance). The ripple drops to 0.01 px. Hard-edged masks
and soft ramps have no sharp edge pixel and give exactly the plain fields.
The GPU compositor takes the bevel distance fields from the CPU through
new FieldKind::BevelInside / BevelOutside (the glow fields are unchanged);
distance_field takes the reach so values past it are not refined, and the
band halo of the bevel fields grows to reach + 9.
* Chisel bevel: edge normals from a line fit, exact at hard stretches (#1926)
A follow-up to the previous commit, from measuring how far it was from the
best it can do. The 3x3 coverage gradient gives an edge's direction to 1-3
degrees, and the error is systematic, so every distance tilted the same way
and left a 0.010-0.017 px ripple. With the true normals the ripple is 0.002
to 0.004 px, so the normal was the limit.
refine_edge_normals fits a line through the edge points (pixel centre moved
to its edge) of the sharp pixels within 3 px that agree to 25 degrees, and
takes the line's normal where the points really are on a line (spread along
it at least 0.7 px, off it at most 0.1 px). Corners and tight curves keep the
gradient's normal. On straight edges it reaches the ideal-normal floor.
The inside field now seeds from the empty pixels, as the plain field does,
and measures to the sharp pixel next to the nearest empty one, capped by the
plain distance (which never falls short of the true one). A circle's pole,
or a rounded rectangle on whole pixels, has a stretch of edge exactly on a
pixel boundary with no sharp pixel; the distance to the sharp pixels alone
was up to 0.29 px too long there. A mask without sharp pixels still gives
exactly the plain field.
Measured on anti-aliased shapes: ripple on straight edges 0.0105 -> 0.0038
(0.0172 -> 0.0021 at 75 degrees); a disc of radius 70, inside, worst error
0.29 -> 0.02. The band halo of the bevel fields is reach + 9 and the banded
test now sees a halo that is too small (the old one passed at reach + 3).
* Chisel bevel: sparse sharp-edge storage, no full-map copies (#1926)
Measured before sending the fix: the new
bevel fields made a bevel size change in the GPU canvas about twice as slow
(12 MP: 84 -> 161 ms per step), and nearly all of a step is the CPU effect
maps. The cost of the normal refinement was memory, not arithmetic: a
Vec<Option<SharpEdge>> is 16 bytes for every pixel, refine_edge_normals
copied the whole map twice for a few thousand edge pixels, and
local_coverage cloned the whole map and read nine pixels for each.
SharpEdges keeps a 4 byte slot per pixel and a short list of the edge
pixels. The candidates are found by a row by row pass over which pixels are
covered (an edge pixel is covered and has an empty neighbour), and the
coverage is worked out where it is needed by the same formula as
local_coverage. The refinement works on the list. Nothing else changes:
the fields are bit-identical (FNV fingerprints of 27 maps at 5 reaches and
of whole bevel_maps at 3 sizes, 216 of 216 equal), and the rendered PNGs of
the issue scene and a realistic scene are byte-identical.
Release, 12 MP ellipse, size 8, one thread: new fields 408 -> 351 ms (old
fields 290); the sharp pass 48 -> 19 ms and the PCA step 32 -> 1 ms.
GPU canvas, ms per size change, main / before / after: 12 MP ellipse
84 / 161 / 96, 300 stars 122 / 246 / 166, text 91 / 184 / 139, 36 MP
ellipse 295 / 522 / 336. What is left on edge-heavy content is the
neighbour search and edge_distance, which this commit does not touch.
* Edit > Stroke: whole opaque pixels; Center puts the odd pixel inside; canvas edge strokes
Center expanded and contracted the selection by w/2, so odd widths left
half-transparent pixels on both sides (a 1 px stroke was a 2 px band at
half opacity), and a selection edge on the canvas border got no stroke
(Select All, Stroke Inside drew nothing). As in Photoshop, widths round up,
Center puts ceil(w/2) inside and floor(w/2) outside, and the mask is padded
so the canvas border is a selection edge.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Edit > Stroke: a hard-edged selection strokes hard, matching Photoshop's curves
Photoshop's 6 px Outside stroke of a 14 px circle selection made without
anti-aliasing is reproduced exactly when a pixel is stroked whenever its
centre is less than width + 1 from a selected pixel's (our expand gave the
6..7 px fringe partial alpha). For a hard selection the band is hardened;
soft selections keep their soft edges.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Edit > Stroke: the inside of a hard stroke matches Photoshop on curves
Photoshop's 3 px and 5 px Inside strokes of a 14 px hard circle, and its 6 px
one filling the circle, fit one rule: inside, a pixel is stroked when its
centre is less than width + 1/2 from an unselected pixel's (the outside rule
is width + 1). Straight edges are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Edit > Fill: Color... and its swatch open the Color Picker
Choosing Contents: Color... opens PhotoCraft's Color Picker (as Photoshop's
Fill does), and the colour swatch opens it too instead of egui's compact
popup. OK sets the Fill colour, not the foreground; Cancel leaves it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Edit > Stroke: the colour swatch opens the Color Picker
A dialog body asks for the Color Picker on its colour field with
color_picker_ui::request; Fill and Stroke share it. OK sets the dialog's
colour, not the foreground.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Opening the preset store decoded every imported tip at full resolution, and
each tip then lived twice: in the session's library and in the store's
snapshot of the last sync. A generated 600-brush library (16-bit tips,
800-2000 px, 2.27 GB decoded) cost about 4.5 GB after import and again after
every restart.
Library presets now hold their bitmaps as StoredTile references with a small
saved preview (at most 96 px): photocraft-paint gains TipShape::Stored and
Pattern::Stored (rendered with the full bitmap when loaded, else the preview),
and the store writes each tip's size and preview with its group. Opening the
store reads previews only; once a group is written its presets drop their full
bitmaps. A tip is decoded when its brush is used (picking a preset, a stroke
naming one, exporting) into a 256 MB least-recently-used cache. A loaded stored
tip serialises as `sampled`, so tool presets, actions and brush.get keep the
same JSON. Stores written before this change have no previews: their tips are
decoded once at the next start, eight at a time, and the groups rewritten. A
missing or corrupt tip file is now reported when the brush is used.
Measured with crates/engine/examples/abr_memory.rs on the 600-brush library
from crates/io/examples/gen_abr_library.rs (Windows, release):
after import 4565 -> 38 MB working set, after restart 4551 -> 20 MB,
after using 5 brushes 4560 -> 37 MB; store open 1.47 s -> 0.05 s.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The app still showed the Adobe Photoshop name in several user-visible
strings. Per the product rule (user-facing text says PhotoCraft only),
none of these mention Photoshop now.
UI labels, tooltips and status messages:
- New Document preset "Default Photoshop Size" -> "Default PhotoCraft Size"
- "All shortcuts reset to Photoshop defaults." -> "All shortcuts reset to
PhotoCraft defaults."
- "Import Photoshop brushes (.abr)" -> "Import brushes (.abr)"
- smart-filter fallback name "Photoshop filter" -> "Smart filter"
- File > Save As .psd/.psb filter "Photoshop" -> "PSD Document"
Errors and warnings:
- "this Photoshop filter isn't implemented in PhotoCraft" -> "this smart
filter isn't implemented in PhotoCraft"
- smart_map: "has no PSD equivalent", "not a recognized filter", "its
filter data is missing/unreadable", "this filter has no PSD equivalent"
- tiff_layers: "the PSD layer data...", "the PSD image resources..."
- multichannel: "32-bit Multichannel isn't supported"
- abr: "not a readable brush file (.abr)"
- gradient import: "not a readable gradient file (.grd)"
Translations: updated the changed tl! sources in all 16 catalogs and
dropped the stale "Load Photoshop brushes (.abr)" entries.
Installer / OS metadata: de-branded the nfpm description, the PSB MIME
comment, the AppStream summary/description, the macOS UTType
descriptions, the Windows ProgId Description and the desktop launcher's
Keywords. The Photocraft.PhotoshopDocument ProgId and the
image/vnd.adobe.photoshop MIME/UTType stay: machine identifiers,
changing them hurts file fidelity and Open-with registration.
Command params docs (agent-facing): dropped Photoshop from the
type_cmds kernPair text, the layer_menu_cmds Advanced Blending defaults
and the mask_view_cmds backslash-key text. The "photoshopManages" wire
value stays. Comments, format identifiers and the "photoshop" theme
alias are out of scope and unchanged.
The macOS native-menu shortcut resolution messages no longer say "as in
Photoshop" (Hide/Minimize).
A layer with Transparency Shapes Layer off takes the whole canvas as its
effect region, so on a 1920 x 103675 page each such stroked layer built
~2.4 GB of effect maps, and a render held them for every layer at once.
Every edit's damage rect rebuilt them: 13 s and 11+ GB for three layers,
past 24 GB for ten.
When such a layer's effects are local (no gradient strokes, Precise
glows, chiselled bevels or noise) and it is composited within the
rectangle being rendered (each band of render_bands, or a damage rect),
build its maps over that rectangle padded by twice the effect reach, and
let them go when the next band starts. Composited over anything wider (a
group's effect shape renders its children over the group's whole region)
it keeps the whole region. The pixels are unchanged: a test renders every
local effect kind, masked and unmasked, inside a group with far-reaching
effects and clipped to a base with a wide glow, at 8/16/32 bits, in bands,
parts and tiles from a cold effect cache, and compares bit for bit with
the whole render.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The History panel footer's trash and file-plus buttons discarded their
clicks. They now dispatch two new engine commands:
- history.deleteState {"state"?}: drops the state and every later one,
as Photoshop's linear history does; the document returns to the state
before and the deleted steps can't be redone. The oldest state can't
be deleted (button disabled).
- history.newDocument {"state"?,"name"?}: a new document holding the
document at that state, named after the state, with a fresh history.
Snapshots aren't implemented, so Create new snapshot is shown greyed.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Like Photoshop, a schema value dialog (Select > Modify, Filter, Image >
Rotate Canvas > Arbitrary, ...) opens with its first number field focused
and its text selected, so typing replaces it and Enter applies it. It
also reopens with the values it was last applied with, kept across
restarts in prefs.dialogs and validated against the command's spec on
the way back in (ranges clamped, unknown choices dropped). Cancel and
failed runs remember nothing. Dialogs that show document state (Global
Light, Color Table, Proof Setup, Assign/Convert to Profile) and Define
Pattern keep their current behaviour.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Thai typed in a font without Thai glyphs (any newly chosen font, which
drops the PSD PostScript name that found the original Thai face) fell
through the fallback stack to .notdef: none of the candidate families
cover Thai on Windows (Segoe UI), and Arial Unicode MS is rarely
installed. Add the platform Thai families (Leelawadee UI / Leelawadee /
Tahoma, Thonburi / Sukhumvit Set, Noto Sans Thai) to the candidates,
ahead of the CJK and last-resort fonts, as Photoshop's missing glyph
protection substitutes an installed font. parley picks the fallback per
cluster, so base consonants and their marks stay in one font.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Help > About > Contributors and Models: the table header stays put while rows scroll, columns are measured once so sorting no longer shifts them, counts are right-aligned and every column fits the 700 pt window. The grab bag drops its stray separators and spaces rows evenly, and Models shows <1% for a model with commits and is as tall as its rows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The wheel over the Layers panel's blend-mode dropdown (closed or open) steps to the next or previous mode, one per notch and one undo step each, stopping at the ends, as in Photoshop. Fractional wheel deltas add up to whole notches, and the wheel there no longer scrolls the panel (#1747).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A successful Save As (UI, automation or headless) renames the document to the written file, so its tab and title stop saying Untitled. The name is file identity, not a history step: undo and redo keep it. Failed writes, Save a Copy and exports leave the name, path and dirty state alone, and a deferred save no longer steals the selected tab.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 4096-entry ramp LUT interpolated across a hard stop, showing grey where the CPU export is
white. Gradient fills, Gradient Maps and gradient effects with coincident colour or opacity stops
now return Unsupported so the canvas uses the CPU oracle. Fixes#974.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Integer DNGs with zlib-compressed strips or tiles (compression 8) now decode, with TIFF predictor 1
and 2 (wrapping at the sample's container). Output is capped at the tile size; floating-point
predictor 3 and lossy DNG keep their unsupported errors. flate2 was already a workspace dependency.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Outer Glow now gets the same fill-dependent knockout as Drop Shadow, on CPU and GPU, so a Fill 0%
shape with a glow no longer paints the glow across its interior. Fixes#1953.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The prefs_usage scan treats everything after a file's first #[cfg(test)] as test code; the channel-tint tests from #1986 sat above export_os_clipboard and hid its read. The test module moves to the end of ui-egui's lib.rs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Quick Selection is now one min cut around the brush footprint (Boykov & Jolly 2001; coarse to fine for
large windows, Lombaert et al. 2005), with costs fitted to Photoshop 25.4's masks measured as a black box
on 105 synthetic and public-domain cases (mean IoU 0.94 synthetic, 0.83 paintings), at Photoshop's
512x512 working-resolution rule. Replaces the colour-model flood, which selected far too much.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Color Lookup properties' Load 3D LUT… entry opens the platform file picker (filtered to .cube, .3dl and .look, on desktop and web) and embeds the chosen LUT into that adjustment layer, replacing the typed-path field. A cancelled or invalid pick leaves the layer unchanged (#1749).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Levels and Curves composed their master and per-channel records in opposite orders (#975). The
Photoshop-authored corpus decides it: channel first, then master. Levels now does that, bringing
psd-tools levels_rgb.psd from 0.447 to 0.012 max error; Curves (curves_rgb.psd passes) is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Desktops that move windows with Alt+drag (Cinnamon, MATE, Xfce, older KDE) grab Alt+left presses, so
the Alt Eyedropper and Clone Stamp source never got the click. The canvas now spots the grab and says in
the status bar what happened and how to get Alt-click back. Fixes#1658, #1659.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Right-clicking a dock tab offers Close (hides just that tab; the last one hides its group) and Close Tab Group, as in Photoshop. Hidden tabs are kept by name in the dock layout, so they survive theme switches and workspace saves, and the Window menu brings a tab back (#1753).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
View zoom now means device pixels per document pixel, so 100% shows one image pixel per physical
display pixel on a scaled (125%, 200%) display, as in Photoshop. Fit, Actual Pixels, pan/zoom anchoring,
scrollbars, the navigator, the pixel grid and every hit tolerance go through canvas_ppp / point_zoom.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Crop options bar's overlay button opens Photoshop's overlay menu: six overlays, Auto/Always/Never
show modes, and O / Shift+O to cycle the overlay and its orientation. The default is Rule of Thirds with
Auto Show Overlay, as in Photoshop. ui.set takes cropOverlay, cropOverlayShow and cropOverlayOrientation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Large Tabs makes the document tab strip taller (34 px) with larger type; turning off Export Clipboard stops copies being mirrored to the OS clipboard; Show Channels in Color tints the Channels panel thumbnails (RGB from black to the primary, CMYK from white to the ink). Settings that do nothing: 48 of 141 (#204).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since #1917 the exporter writes pattern fill pixels; a vector-masked pattern fill then reopened as a
shape layer covering the canvas, failing the corpus round-trip floor (adjustment-fillers.psd).
Pattern-filled plain shapes now import as pattern fill layers with their vector mask.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With a pending crop frame, the arrows move it 1 px (Shift 10 px) instead of the selection or layer, X
swaps it between portrait and landscape about its centre (with the options-bar ratio), and the frame's
W x H shows beside the pointer while drawing or resizing it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A schema-built filter dialog reopens with the values it was last applied with, as in Photoshop. Only schema-valid numbers, choices and booleans are kept (no paths, document indices or free text), and a failed or cancelled dialog saves nothing (#1757).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A pending crop frame belongs to the document it was drawn on: New, Open, a tab switch or closing it
cancels the frame, so it never shows on or crops another document. While a crop is pending, the menus
Photoshop greys are greyed (checked item by item in Photoshop 27.11), and Image > Crop commits it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>