The dialog's histogram fitted its tallest interior column to the plot, so one spike (a dark sky, a bright wall) flattened the midtones' hill to a few pixels. Camera Raw 16.1.1 scales linearly by the typical columns and lets a spike run off the top (measured on Photoshop 25.4: 49 px per 10 000 pixels in a 106 px plot, the 95th-percentile column at 14 546, the tallest at 21 600).
`rgb_histogram::Spikes`: `Fit` (the old behaviour, every other histogram keeps it) and `Clip` (the scale is at most 1.5 × the 95th-percentile column, taller columns clip at the top). The Camera Raw dialog's histogram uses `Clip`.
Test: `camera_raw_scale_lets_a_shadow_spike_run_off_the_top`.
Split out of #3816 at the reviewer's request.
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
`develop::tone` interpolates the camera curve logarithmically between knots, and a curve whose first knot sits at black (0, 0) made the first segment divide by log₂ 0: every value in it came out NaN, and `retone` (the open-time Shadows/Highlights preview) propagated it into the pixels. From a knot at black the segment is now linear, where log₂ has no value; the other segments are unchanged.
Test: `retone_lifts_shadows_with_a_curve_from_black` (finite output, shadows lifted, highlights kept) fails without the fix.
Split out of #3816 at the reviewer's request.
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Testing on a 6000 × 4000 document found the proxy's sampling state computed
per pixel against every stroke segment: each stroke got slower, and an undo
tested 1.7 M pixels × 3000 segments on the UI thread, blocking it for over a
minute. The proxy is now painted as the engine paints the full-resolution
mask: the option's area and the fill area are sampled once at the proxy's
pixel centres (set_base), and each stroke is painted over them in order,
visiting only its own pixels (paint, restate_all), so a dab costs its segment
and a rebuild costs the strokes' areas.
Also from the same review:
- `edit.contentAwareFill {"ui": …}` with the workspace closed is an error
instead of a fill with the defaults (an agent polling the state after an
Esc added a fill per poll).
- The view sends one Down per press: the press is the stroke's first point
and the frames before egui's drag threshold are its next ones, instead of
drag_started() beginning the stroke over (the lasso lost its first points
the same way; the Zoom tool zoomed on every frame the button was held).
- The preview texture is box-averaged to the device's max_texture_side when
the plan's rectangle exceeds it (a 17000 px wide plan gave a validation
error every frame).
- A preview that fails counts as shown for that state (Preview::finish), so
it is reported once instead of restarting on every repaint.
- The view shows the document's visible layers composited (an Apply to a new
layer shows); the engine's limits on strokes, lasso edits and points hold
at the brush with a message instead of failing Apply; the footer is
Photoshop's reset, OK, Apply, Cancel on every platform; a leftover
`let _ = i;` is gone.
Tests: strokes painted once and equal to the engine's sampling area; `ui`
with the workspace closed; the first points of a stroke and a lasso through
egui_kittest; the preview texture at a 2048 limit; a failing preview
reported once; the view after Apply to a new layer; the limits at the brush.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Conflicts:
- distort_ui.rs: main's `drop_foreign` (#5092, #5123) closes the Content-Aware Fill workspace
too when another document becomes active or its own is closed, stopping its background
preview as Cancel does; what Apply wrote stays. New test
`the_workspace_is_dropped_when_another_document_becomes_active`.
- sr, tr, vi catalogs: both sides' rows. Dropped es's High, Low, Duplicate Layer and Contract,
which main has now with the same translations.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Conflicts:
- edit_menu_cmds.rs: main's #4133 (Sample All Layers off unless the active layer has no pixels
in the window) now decides `sampleAllLayers` inside the workspace's fill plan; the job keeps
the plan-based fill, the spec keeps the workspace's parameters.
- lib.rs: both module lines (caf_ui, cam_preview).
- hu, tr, vi catalogs: both sides' rows. Dropped de's High/Low (main now has them) and hu's
older "Vanishing Point" row main reworded. Added the 15 workspace strings to sr.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Camera Raw: Whites, Blacks, Shadows and Highlights closer to Photoshop's
Measured as a black box on Photoshop 25.4's Camera Raw Filter.
Whites and Blacks are global curves of the grey level: the measured rows
per slider value, Whites first, after Shadows and Highlights. The
clipping directions adapt to the image's black and white points
(Whites up to 2.7 times stronger on a dark image, Blacks at least 1.2).
On ten public-domain photos the mean error against Photoshop drops from
4.2-36.8 to 0.9-8.9 levels of 255.
Shadows and Highlights are a local Laplacian filter (Paris et al. 2011,
fast form of Aubry et al. 2014) on the log luminance, the method Adobe
credits for process version 2012. Shadows raises pixels darker than
their surroundings by an amount set by their own and their
surroundings' tone, so big edges keep no halo, and keeps the 99th
percentile; Highlights pulls pixels brighter than their surroundings
toward them and keeps the 1st. The strengths are fitted to the photos
(mean error 2.8-9.4 levels, was 3.7-14.3); Photoshop's exact operator
was not recovered.
The tone stages decode light with the document's curve: filter.cameraRaw
stores encodingGamma (1.8 for ProPhoto, where raws open).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Camera Raw Shadows/Highlights: no contour lines on smooth gradients
Twelve reference values over a raw's whole range left more than a stop
between them, and the remapping changes within half a stop: coefficients
interpolated across that gap drew contour lines in out-of-focus areas
(Shadows +100 on a Nikon D4 raw). References are now an eighth of a stop
apart over the image's tones (the deepest 0.1 % share the lowest), on a
copy at most 1600 px across whose change comes back to full size by a
guided upsampling, so a 16-megapixel image takes about 2 s per slider and
the preview matches the result.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Camera Raw on raw opens: Shadows/Highlights in linear light, D4 look and black point as Photoshop
Measured black-box on 29 Nikon D4 NEFs opened through Camera Raw in Photoshop 25.4 (sliders at
+-100 and +50, exposure +-1) against our own linear development of the same files.
- Shadows and Highlights of the open-time dialog now act while developing, in linear light before
the tone curve, as Camera Raw does for raw files (exposure -1 gives the same gains). The log2
gain is slider/100 * C(B - anchor): B is a guided-filter base of the log2 luminance on a copy
1232 px on its long side, the anchor is image-adaptive (Shadows: log2 power mean p=1.5 of the
linear luminance; Highlights: mean log2 luminance). New module photocraft_raw::tone; the
preview undoes the camera curve to show the same (photocraft_raw::retone).
- The D4 profile copies Camera Raw's default rendering: look matrix folded into the
ForwardMatrix, per-channel curve, and an adaptive black point (0.33 x the 0.1th percentile of
the luminance, Sensor::black_point).
Against Photoshop on the 29 frames (1/4 size, log2 Y): default rendering median difference
0.29 -> 0.04 stop; correlation of the slider's effect Shadows +100 0.96, -100 0.91, Highlights
-100 0.87, +100 0.85 (the previous filter model: 0.68 and 0.55 on DSG_3464).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* docs: the open-time Camera Raw re-develop covers Shadows and Highlights
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Camera Raw raw-stage Shadows: keep shadow contrast as Photoshop does
The user saw Photoshop's shadows sink deeper with more contrast. Measured on the 29 D4 NEFs:
with Shadows +100 our shadow detail had 0.79 of Photoshop's local contrast and shadows sat 0.38
stop lighter. Two causes:
- The black point now comes before the Shadows / Highlights gain, as in Camera Raw (lifting
first let the lift wash out the black point's deep-shadow contrast).
- The guided upsampling's epsilon goes from 0.01 to 1 stop^2: the change follows strong edges
but stays smooth inside texture (with 0.01 it followed every detail and flattened it). Near
strong edges the effect still matches Photoshop as well as elsewhere.
Shadows +100: shadow detail 0.79 -> 0.91, shadow bias +0.38 -> +0.16 stop; effect correlation
Shadows -100 0.906 -> 0.946, Highlights -100 0.870 -> 0.886, others unchanged.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Camera Raw on raw opens: Clarity, Texture and Dehaze in linear light, as Photoshop
Measured black-box from Photoshop 25.4 opening 29 Nikon D4 NEFs through Camera Raw (descriptor
keys Cl12, CrTx, Dhze; each at ±100 and +50, and +100 at exposure −1, 348 opens). All three act
before the tone curve: the change at exposure −1 equals the one at 0 (correlation 0.98–0.99).
- Clarity: log2 luminance split into bands between guided-filter bases (radii 2, 8, 32, 128 on
the 1232 px work copy), each band scaled by a measured gain that follows its base's tone, plus
a tone term; carried to full size with the Shadows / Highlights change.
- Texture: Gaussian bands σ 0.5…16 on a half-size copy, each through its measured response
(small detail raised more than strong edges, flat areas unchanged), guided-upsampled.
- Dehaze: He, Sun and Tang's dark channel prior on linear ProPhoto (dark channel in Display P3,
airlight from the brightest 0.1 % of it, guided-smoothed transmission), w 0.8 at +100;
−100 a uniform veil. The black point stays the default rendering's.
Against Photoshop, PhotoCraft's develop vs Photoshop's open on all 29 frames (median effect
correlation, log2 luminance at 1/4 size, Texture at 1/2): Clarity +100 0.81, −100 0.85;
Texture +100 0.85, −100 0.88; Dehaze +100 0.61 (0.78 in gamma 2.2: the crushed shadows
differ), −100 0.96. Shadows / Highlights unchanged (0.964 / 0.886).
The open-time Camera Raw dialog re-develops for them and its preview shows them (retone).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Image › Adjustments › Shadows/Highlights follows Photoshop 25.4, measured as a
black box: per-channel screen curves (1 - (1 - v)^q blended over q = 1, 2, 4,
8), the Tone mask as a Beta distribution function of an 8-bit neighbourhood
brightness, an edge-aware (guided bilateral) neighbourhood, the luma of the
result with the colour differences scaled by Color, and the Midtone S-curve.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Since the shared docking migration (#3097) the tool column's scroll area
filled the toolbar's whole height, so a window taller than the tools
pushed the colour chips, Quick Mask and Screen Mode to the bottom.
Photoshop keeps them right under the last tool. The scroll area now
shrinks to its tools and only a column taller than the window scrolls,
with the chips kept at the bottom as before.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- The fill area and sampling area (`content_aware_plan_with`) are worked out
inside the background job, cancellable row by row as #3395 made the rest of
the fill: the window and selection are read in parallel bands, Auto sampling
is `auto_sampling_with`. The params are still checked before the job starts.
The workspace's preview builds its plan with its own cancel flag.
- The apply step is #3395's: only the fill area's bounds keep the original
pixels and coverage. `CafPlan::layer_px` is kept only when Sample All Layers
samples something else than the layer.
- `fillEdits` and `samplingStrokes` take at most 1000 entries and 200 000
points in all (MAX_CAF_EDITS, MAX_CAF_POINTS).
- `paint_strokes` leaves a mask that does not cover its area alone, writes
through `get_mut`, and visits each segment's own bounds instead of the whole
stroke's for every segment (the same pixels). The workspace's
`sampled_at`, `in_hole` and outline read their buffers with `get`.
- Content-Aware Scale's transform box (#3096) and the Vanishing Point
workspace keep their menu entries next to the Content-Aware Fill workspace.
- Vietnamese labels for the workspace.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Content-Aware tools: faster, cancellable at once, same pixels
#2202 matched Photoshop's Content-Aware Fill but made the perf nightly slower (#2575): P21
Content-Aware Fill 10 MP 5x, P22 Spot Healing 2.4x, P49 cancel on 24 MP 4x and over its 200 ms
budget. This keeps every result bit for bit and takes the time back:
- The solver works over the hole's bounds grown by a patch: match field, costs, votes and the
per-target tables are kept there (nothing outside changes or is matched), the vote computes
each target's weight once and writes into a buffer instead of a Vec per pixel.
- Colour adaptation reuses each patch's own moments (target sums kept per EM step, source sums
per level where they pay off), summed in the same order as before.
- Exact early outs: SSD per pixel instead of per row (callers only ask "better?"), and the onion
peel's exhaustive search starts from where the filled neighbours took their sources and stops a
source once it can only lose (first-of-equals kept).
- PatchMatch sweeps a band in wavefronts of blocks when there are fewer bands than threads: each
cell sees the same neighbours and random numbers (SplitMix64 skip) as in raster order.
- Parallel pyramid, valid-source and target setup; no copy of the window when there are no
variants (fill_owned / complete_masked_owned); the job reads the window in parallel bands and
keeps only the selection's bounds for the apply step.
- Cancellation is checked per row everywhere, also in Auto sampling and in its max flow
(maxflow_with / grid_cut_cancellable): a 24 MP job now stops in tens of milliseconds.
- algo::par: rayon on native, sequential on wasm.
Proof: the new `content_aware_bench` example hashes every tool's output (Fill with each sampling
and adaptation option, Edit > Fill, Delete and Fill, Spot Healing x3, Patch, Content-Aware Move,
8/16/32-bit, Gray/CMYK/Lab, and the benchmarks' 10 and 24 MP fills): 117 hashes identical to
main, also with 1, 2 and 4 threads. New tests compare the onion peel with a plain scan, full and
clipped patch distances and moments, the valid-source runs with the summed-area test, thread
counts 1/3/16, in-place and copied fills, and cancellation of Auto sampling and max flow.
perf_scenarios --cpu, Ryzen 7 5800X shared with other work (load 4-7), 3 interleaved rounds:
P21 p50 295-306 -> 167-171 ms (max 342-362 -> 180-184); P22 p50 67-68 -> 38-40 ms (max 69-71 ->
40-41); P49 max 378-389 -> 54-77 ms (p50 94-110 -> 47). retouch_bench: Content-Aware Move
-47..-64 %, Spot Healing Content-Aware -40..-61 %.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Content-Aware: faster Spot Healing dabs than main with #2805, same pixels
Against main with #2805 the branch was slower for small Spot Healing dabs (size 50:
+25-30 %, single-threaded +60 %): its onion peel walked all 49 offsets of every candidate patch,
checking each against the known pixels, and had no bound at the hole's edge.
- Onion peel: the target's known pixels are gathered once (#2805), the search starts from the
filled neighbours' shifted sources or every 16th source at the hole's edge (#2805), the limit
a candidate must stay under is a precomputed sum (no division per pixel), and candidates are
summed four at a time in step: their sums are independent, so the processor overlaps them.
Each is still given up only against the best from before the four, which can only improve.
- Vote: each target's weight and match centre are taken once; a pixel's patches are visited
over the clipped offset range, with the channel count and the colour adaptation fixed at
compile time. Same sums, same order.
- Valid sources: sliding counts per row and stacked runs per column instead of a 7-row check
per pixel; the k-th source is found through per-row and per-64-column counts, so no level
but the coarsest lists them all.
- `integral` stays out of line: inlined into `synthesize` it made Create Texture slower.
Same results: `content_aware_bench -- hash` (112 cases) and `-- hash-big` (the benchmarks' 10 and
24 MP fills) equal main's, also with 1 and 3 threads; the onion peel tests (#2805's and ours)
compare with the plain scan. `content_aware_bench bench spot` runs retouch_bench's Spot Healing
dabs; with `CAF_SPOT_TYPE` (comma-separated) other types, with page faults per dab.
Ryzen 7 5800X, shared (load 5-20), interleaved with main (3af4c46a), min of rounds,
retouch_bench, all threads / one thread:
Spot Healing Content-Aware 50: 49 -> 31 / 204 -> 117 ms; 100: 62 -> 35 / 164 -> 98 ms;
200: 118 -> 67 / 250 -> 158 ms; 300: 212 -> 119 / 411 -> 285 ms. Content-Aware Move -47..-62 %
(one thread -23..-45 %). Rows that don't run this code stay within noise, except Proximity
Match 200 after the Content-Aware dabs in the same process (+8..23 %): it takes ~5800 page
faults there against ~300 on main, because main's heavier dabs left more of the heap faulted
in; alone, or after Content-Aware dabs in `bench spot`, it is equal.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Image Size: Photoshop's resampling, measured
Photoshop 25.4's Image Size was measured as a black box (synthetic 16-bit
images in, exact RAW out); every method but Preserve Details' enlargement is
linear, so each was taken apart from its impulse responses.
- Bicubic is the Keys cubic at a = -0.75, not Catmull-Rom.
- Bicubic Smoother (stretched x1.15, a = -0.625) and Bicubic Sharper
(x1.05, a = -1), with a growing with the reduction, and Bicubic Automatic.
- Reducing widens the filter by 1/s; below 1/4 the image is first averaged
down to four times the target. Bilinear is never widened and averages
whole pixels below 1/2. Positions are rounded to 1/128 pixel and stop at
the edge pixel's centre.
- Two passes, the smaller scale first, clipped and stored between them; the
bicubic filter is designed for the height's scale and serves both axes.
Bicubic Automatic splits a mixed resize into Sharper, then Smoother.
- Image Size and Fit Image take automatic, bicubicSmoother, bicubicSharper
and bicubicAutomatic; the dialog lists Photoshop's methods and opens on
the interpolation preference.
On 225 probes the mean error falls from 240-550 to under 0.5 levels of
32768; 8-bit results are identical to Photoshop's for 99 % of values.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Format: one rustfmt change main needs
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Content-Aware tools: Photoshop's behaviour, measured
Photoshop 25.4 was measured as a black box (synthetic and public-domain
images, results read back exactly) and the content-aware family follows it:
- Fill (inpaint): coarse to fine with the coarser level's matches scaled up
and voted with the finer level's own pixels, the coarsest level filled
from the hole's edge inwards, a shallower pyramid. The fill is pieces of
the image copied pixel for pixel with narrow seams, as Photoshop's;
before, it was visibly blurred. Pixels have roles (hole, context,
source, void), so excluded areas stay context instead of being filled.
- Colour adaptation: Image Melding's per-patch gain and bias, limited per
level, instead of a low-frequency membrane pull.
- Sampling window: Photoshop's square of side 4·√(max(w,50)·max(h,50))
centred on the selection, slid inside the canvas; Edit › Fill,
Rectangular, Auto, Delete and Fill and Spot Healing use it.
- Auto sampling area: what looks like the selection's surroundings (local
colour and contrast, one min cut), fitted to Photoshop's overlay.
- Patch Tool: a Content-Aware mode (Structure, Color, Sample All Layers).
- Content-Aware Move: Structure 3..7 alike, a band about a patch wide;
Color as a gain/bias limited to about 0.04 per step.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Remove tool: keep its own sampling margin (fill_cmds::sampling_margin is gone)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Edit › Content-Aware Fill… opens Photoshop's workspace (measured on 25.4)
instead of the generic dialog: a full-window dialog like Liquify's.
- Tools: Sampling Brush (B), Lasso (L), Hand (H), Zoom (Z), each with its
options bar.
- Document view: the sampling area tinted green at 50 % and the fill area's
marching ants.
- Preview panel: the fill computed in the background after each change,
opening framed on the sampling window.
- Content-Aware Fill panel: Sampling Area Overlay (Show, Opacity, Color,
Indicates), Sampling Area Options (Auto, Rectangular, Custom, Sample All
Layers), Fill Settings, Output Settings, and the reset buttons.
- OK, Apply, Cancel.
As Photoshop's:
- The Sampling Brush subtracts in Auto and Rectangular and adds in Custom,
which starts empty; Alt reverses. Its first size is a tenth of the
window's side. Painting keeps the option, and added strokes may lie past
the window.
- The lasso edits the fill area (New, Add, Subtract, Intersect, Expand,
Contract); the window and the Auto area follow.
- Apply fills and stays open; Cancel keeps what was applied. Ctrl+Z undoes
a stroke or an edit. Output opens on New Layer; settings are remembered.
The workspace holds plain data. `edit.contentAwareFill` takes it as
`samplingStrokes`, `fillEdits`, `sampleAllLayers` and `layer`, and both
the preview and OK run one plan (`content_aware_plan`), so the preview is
what gets written. New strings are translated in every catalog.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Photoshop 25.4 was measured as a black box (synthetic and public-domain
images, results read back exactly) and the content-aware family follows it:
- Fill (inpaint): coarse to fine with the coarser level's matches scaled up
and voted with the finer level's own pixels, the coarsest level filled
from the hole's edge inwards, a shallower pyramid. The fill is pieces of
the image copied pixel for pixel with narrow seams, as Photoshop's;
before, it was visibly blurred. Pixels have roles (hole, context,
source, void), so excluded areas stay context instead of being filled.
- Colour adaptation: Image Melding's per-patch gain and bias, limited per
level, instead of a low-frequency membrane pull.
- Sampling window: Photoshop's square of side 4·√(max(w,50)·max(h,50))
centred on the selection, slid inside the canvas; Edit › Fill,
Rectangular, Auto, Delete and Fill and Spot Healing use it.
- Auto sampling area: what looks like the selection's surroundings (local
colour and contrast, one min cut), fitted to Photoshop's overlay.
- Patch Tool: a Content-Aware mode (Structure, Color, Sample All Layers).
- Content-Aware Move: Structure 3..7 alike, a band about a patch wide;
Color as a gain/bias limited to about 0.04 per step.
Co-Authored-By: Claude Opus 5.5 (1M context) <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>
* Canvas zoom: Photoshop's range, ladder and ⌥-wheel step; no slide at the limit
Measured on Photoshop 25.4 (exact zoom read by script, view position by screenshot
registration):
- ⌥ + wheel: ×1.1 a notch around the pointer (was ×1.05);
- the zoom runs up to 12800 % and down to where the document's shorter side is 2 px; a wheel
notch past a limit lands on it exactly and further notches change nothing;
- Zoom In / Zoom Out and a Zoom tool click step along 0.1 … 66.67, 100, 200 … 800, 1200, 1600,
3200, 6400, 12800 %, from any zoom to the next rung;
- with Overscroll off, an image smaller than the view stays where zooming put it, inside the
view, instead of being re-centred.
PhotoCraft capped the zoom at 3200 % without meaning to: the status bar's zoom field (and the
Navigator's slider) clamped it and wrote it back every frame. Past 3200 % each ⌥-wheel notch
zoomed around the pointer and the field pulled the zoom back, so the image slid towards the
pointer. All zoom paths now share `zoom_levels` (range, ladder, clamp), and the fields always
include the current zoom.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Zoom: Print Size, 200 % and Fit Layers on Screen clamp through zoom_levels too
Review of #1718: `view.twoHundredPercent` / `view.printSize` and `view.fitLayersOnScreen` still
clamped to 1-6400 % on their own. They now use `zoom_levels::clamp` like every other zoom path:
Print Size at 1 ppi gives 7200 % (it was cut to 6400 %), and fitting a 1 px layer stops at
12800 %.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Wheel zoom: a notch applies on the frame it arrives (from #1490)
Folded in from @Jeremy8776's #1490, as agreed in review: zooming reads this frame's wheel
events (`Input::raw`) instead of egui's smoothed delta, so each notch is one ×1.1 step on the
frame it arrives, with no easing over the next frames (his frame-by-frame recordings of
Photoshop). The smoothed tail neither zooms nor turns into a pan when Alt is released; panning
still uses the smoothed delta. Zoom with Scroll Wheel uses the same ×1.1 notch (assumed, not
measured).
The Filter Gallery's and Liquify's own preview zooms keep their 3200 % caps, now with a comment
saying why: they aren't the canvas.
Co-authored-by: Jeremy <jeremy8776@googlemail.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Jeremy <jeremy8776@googlemail.com>
The colour chips sit centred in the toolbar with Default Colors and Switch Colors above them. The Color Picker gains L/a/b radios and the out-of-gamut and web-colour warnings, each with a click to take the nearest colour. Panel footers are a full-width bar with their own mask and adjustment glyphs, keeping the right-click footer menus from #1680.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1066 added the gesture; this makes it behave as Photoshop 25.4 does (measured by sweeping
⌥-hover and ⌥-click a pixel at a time across each line, results read back by script):
- the line takes the pointer from 3 px above it to 5 px below it (was ±3);
- a release frees the layer above the line together with the clipped layers stacked above it,
in one history step, and makes it the active layer (was: that layer alone); a clip keeps the
active layer, as before;
- clipping and releasing have their own cursors: PhotoCraft's original clip-below (a hooked
arrow onto the layer below) and clip-release (the same arrow with a small "no" badge),
replacing Lucide corner-right-down for both.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A crop with Delete Cropped Pixels off keeps what it cuts away (and a moved layer can reach past
the edges), but the Crop tool drew only the canvas, so a frame couldn't be pulled back out over
those pixels by eye. Photoshop 25.4 (observed) grows the view to every layer's bounds and the
frame from the first press on the frame: hidden pixels under the shield, transparency as the
checkerboard, no canvas border or shadow, until the crop is committed or cancelled.
- crop_ui: `CropState::editing`, set by the press (before egui's drag threshold) and cleared when
a new default frame is made (commit, Esc) or the tool is left; `shows_beyond_canvas`.
- canvas: `draw_beyond_canvas` composites only the ring between the canvas and every layer's
pixels (`extra_cmds::reveal_all_bounds`, shared with Image > Reveal All), once per document
state, at most MAX_TEXTURE texels per side (from a downsampled proxy past that), and draws it
with the checkerboard out to the frame under the canvas, reaching a point under its edge so the
edge's anti-aliasing has no seam. The drop shadow and the Line border are off meanwhile.
- compose: `render_reduced_rect`, the reduced render of any area of the document plane.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
NEF carries no colour description, so D4 frames developed with the camera channels taken as
sRGB primaries and no tone curve (dark, pale), the sensor's optical-black columns showed as a
purple stripe on the right, and clipped highlights turned pink (green saturates first).
crates/raw/src/cameras.rs holds camera profiles measured from the cameras' own output (no
third-party tables), applied to TIFF/EP raws by make, model and sensor size. Sensor gains
tone_curve (like DNG ProfileToneCurve, per channel in linear ProPhoto), folded into the gamma
LUT; other cameras develop exactly as before.
Nikon D4, from 148 uncompressed NEFs of one body (ISO 100–10000):
- image area 4928x3280 at (8, 6): where the camera's own full-size JPEG sits, found by aligning
the two (2 columns of optical black on the left, 50 + 8 on the right);
- black 0 (the data is black-subtracted in camera: the darkest image pixels reach 0);
- white 15520: green saturates at 15546–15672, red and blue at 15687–16383, so one level just
below green makes clipped highlights white without moving the white balance;
- ForwardMatrix (white-balanced camera → XYZ D50) and tone curve fitted to the embedded JPEGs
(Picture Control Standard, Active D-Lighting off; 52 frames, smooth areas). On 15 held-out
frames PhotoCraft's development is 7.0/255 per channel from the camera's JPEG on average,
against 34 before.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Measured on Photoshop 25.4 (Image > Adjustments > Color Balance and the adjustment layer's
Properties): a double-click on a slider (knob or track; Windows double-click counting) zeroes it
in the shown tone, whatever modifier is held; clicks and drags ignore Shift/Ctrl/Alt; the
dialog's sliders step 1 per wheel notch (Shift 10), the panel's ignore the wheel. Holding Alt
turns the dialog's Cancel into Reset (every tone 0, Midtones shown, Preserve Luminosity kept,
dialog stays open). In the Properties panel consecutive edits are one history step, and Reset
first returns to the settings the panel found, then to the defaults. Up/Down in the fields are
every value field's since #1326 (1, Shift 10, as Photoshop); the tests check them on Color
Balance, below zero too.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Live previews of large documents (filter dialogs, adjustment dialogs that fall back to the
filter preview, zoomed-out adjustment dialogs, an adjustment layer's sliders while they drag)
render on a nearest-neighbour reduced copy: a 16 MP photo previews at every 3rd pixel, drawn
3x blocky at 100 %. Turning the setting off renders them at full resolution.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Saturation −100 greyed to the HSL lightness, so a yellow (255, 255, 0) became grey 128 where
Photoshop shows near-white: a yellow logo with Brightness/Contrast +150 and Vibrance › Saturation
−100 clipped to it stayed yellow under Lighter Color. Measured black-box on Photoshop 25.4
(16-bit runs of 7,400-colour charts and HSV sweeps; sRGB, Adobe RGB and ProPhoto documents), both
sliders work in linear light through the document tone curve, Vibrance first, then Saturation:
- Saturation moves each colour from the grey 0.288 R + 0.712 G (blue has no weight, in every
profile) by 1 + s, clipping per channel: within 0.07/255.
- Negative Vibrance is an HSV change in linear light keeping the hue, in closed form: 0.2/255 on
average, the deepest shadows (V < 0.05) drift.
- Positive Vibrance: the brightness term is exact; the saturation boost (stronger for dull and
dark colours, damped on reds and skin) is a fit, 0.4 (+10) to 5 (+100) /255 on average.
CPU (compose::adjust::vibrance_px) and GPU (compose.wgsl) share the constants; the parity test
gains a negative Vibrance and Saturation −100. photocraft-corpus adjustments/rgb8|rgb16/
vibrance.psd (Vibrance +60, Saturation −15): 25.6/255 mean, 89 max → 0.38 mean, 5 max.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Color Range: Photoshop's Sampled Colors model and the eyedropper on the canvas
Sampled Colors now compares colours in Lab (through the document's profile) the way
Photoshop does, reconstructed from Photoshop 2024's own masks (observation only):
per-axis distance outside the samples' Lab box in 8-bit units, a/b weighted x3, the
largest of the three, and a quadratic B-spline falloff of width fuzziness*256/255.
On 8-bit sRGB images it matches Photoshop to under one level of 255 on average
(the previous per-channel RGB distance was off by about 12). Several samples span
one Lab box, like Photoshop's Minimum/Maximum.
While Color Range is open, the pointer over the free canvas is its eyedropper:
a click picks that pixel, Shift adds to the sample, Alt subtracts (also through
ui.pointer on the control channel).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Color Range: Photoshop's Subtract from Sample, click order, 8-bit Lab grid
Measured through Photoshop's real Color Range dialog (eyedropper clicks):
- Subtract from Sample reshapes the samples' Lab box per axis: inside it the
nearer edge moves onto the subtracted value; on an edge or within the axis
width w it moves in to the first of m + k*w/5 beyond it (k <= 5); further out
nothing changes; an edge never crosses the other. Matches every measured box
to +-0.05. The click order matters, so select.colorRange takes an optional
`order` list and the dialog records it.
- The a/b centring shift of the previous change was an artefact of scripted
samples (Photoshop's descriptor Lab scale); with the eyedropper the sampled
colour is fully selected at any fuzziness. Removed.
- 8-bit documents compare pixels and samples on the 8-bit Lab grid, where an
a/b step above the samples' range counts half a unit more and one below half
a unit less (lab_range_coverage_8bit).
On Photoshop's eyedropper masks of a photo the mean error is ~0.46 levels of
255 at fuzziness 40/80 and ~0.02 at 5/10 (previous change ~0.9).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>