Photoshop 27.11's round tip, measured on single dabs (hardness 0-100 in
steps of 10 at 40 px, checked at 100, 9, 7, 5, 3, 2 and 1 px): a solid
core of radius r0*R, then a Gaussian shoulder exp(-((d - r0) / s)^2) of
width s*R, both from a table by hardness. A soft tip reaches well past its
nominal radius (hardness 0 is a Gaussian of sigma 0.47 R: 8 % at 1.05 R);
PhotoCraft's (1 - t^2)^4 ended at R and kept far less coverage (a 40 px
hardness-0 dab held 251 coverage units against Photoshop's 542). The edge
stays at least ~1 px wide (0.3 R on small tips, never under 1/4 px).
exp is (1 - u/64)^64 by squaring, so it is the same on every platform.
Dab rectangles, reaches and the retouch footprint grow to the tip's reach
(tip_reach); the brush cursor's 50 % ring (half_coverage_radius) follows the
new curve. Tests that hard-coded the old curve now check measured values;
tests that bounded a stroke by the nominal radius use the reach.
Measured against Photoshop (max levels / mean): 40 px dabs, hardness 0-70:
70-156 / 12-15 -> 2-6.5 / <0.7; 100 px dabs 70-177 -> 2-27; strokes
(probe 13) soft and medium 20-60 px 133-178 / up to 17 -> 4-26 / <=0.5;
spacing sweeps 134-221 -> 7-9; soft Flow build-up 85 / 7.5 -> 5 / 0.16;
small 1-7 px total error lower in 21 of 30 cases (medium hardness at
2-5 px a little higher: a follow-up).
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
A shape layer's thumbnail shows its pixels like any other layer, so nothing
in the row said it was a vector shape. It now carries a badge in the
bottom-right corner, in the same frame as the smart-object badge: a path
with an anchor point on each corner.
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
Free Transform and Warp sampled with Catmull-Rom (Keys a = -0.5).
Photoshop's Bicubic is the Keys cubic at a = -0.75 (Image Size already
uses it): a 200 % step edge matches within 0.5 levels (Catmull-Rom was 10
off). When a Bicubic transform reduces, the source is now resized first by
the whole local scale with Image Size's widened Bicubic, and the warp
samples it at about 1:1; the old pre-reduce only kicked in below 50 % and
by powers of two, so 50-99 % reductions aliased. Bilinear still samples
directly (it matched Photoshop exactly at 50 %).
Measured against Photoshop 27.11 Free Transform (max levels / mean):
Bicubic 200 % 10 / 0.36 -> 1 / 0.01; 50 % 48-59 / 1.0-1.2 -> 6-8 /
0.02-0.05; 150 % mean 0.59 -> 0.17. Bilinear unchanged. Bicubic
Smoother / Sharper / Automatic (not offered by Transform yet) and rotated
edges are follow-ups.
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
* Cursors: the pen tools show their icon, and Caps Lock swaps it for the crosshair (#4639)
The pen tools fell through to the canvas's default crosshair whatever
Preferences > Cursors > Other Cursors said, and Caps Lock did nothing.
In Photoshop the Standard cursor is the tool's icon, Precise is the
crosshair, and Caps Lock swaps between them.
The Pen, Freeform Pen, Curvature Pen, Add/Delete Anchor Point and
Convert Point tools now draw their toolbar icon with the nib (Convert
Point: its anchor) on the point. precise_other_cursor (the preference,
flipped while Caps Lock is on) decides it, and the Eyedropper, Color
Sampler and Paint Bucket, which already followed the preference, now
swap with Caps Lock too.
Test: caps_lock_swaps_the_other_cursors_between_their_icon_and_the_crosshair.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Canvas: keep pipette_cursor's doc comment on pipette_cursor
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
* Update Japanese translations in ja.tsv
I have added items that appear to have been omitted from the registration.
* i18n(ja): keep the rows this update dropped
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
The in-dialog preview clamped the texture UV to the image but still drew
it over the whole preview region. Whenever the region overhung the image
on one axis (a 100% view of a small document in the wide preview), that
axis was stretched to fill it. The preview now draws only the part of the
source rectangle that lies on the image, at its own place and scale in the
region, with the checkerboard under the image only.
Co-authored-by: Brandon Thomas <[email protected]>
The picker covers every screen and stays on top, and a report on #4291
describes one that ignored Esc, right-click and left-click, so the
computer could only be freed from another device. In the picker's frame:
- Esc only worked once Windows gave the picker keyboard focus, which
nothing asked for. The picker now takes focus on its first shown frame.
- A right-click did nothing; it now cancels.
- A left click was dropped when no snapshot pixel mapped to the pointer,
and a press that moved before its release never counted as a click.
Any left release over the picker now ends it: it picks the pixel under
the pointer, or cancels when there's none.
The frame moved into screen_picker::frame so it can be tested.
Test: every_way_out_of_the_picker_ends_it.
Co-authored-by: Claude Opus 5.5 <[email protected]>
blend_into takes every tile in the selection's bounds and blended each
one into a fresh copy. History holds the previous document, so
Arc::try_unwrap always failed and a tile with nothing selected was
deep-copied too, unchanged: a 4 px Edit › Stroke on Select All of a
20 MP layer stored +82.5 MB per step for a ring of edge tiles.
blend_tile skips every pixel whose weight (selection × opacity) is
zero, so such a tile comes out byte-identical: keep the tile itself.
Mask::empty sits beside Mask::full. Measured on the same layer: the
stroke step now stores +17.5 MB and a 4000×3000 ellipse fill +14.2 MB
instead of +26.0 MB. The new test fails on the old code.
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
channel_thumbs was keyed by the document snapshot, so every step,
including a marquee or Deselect, recomposited the whole document for the
56 px thumbnails (`compose::thumbnail`, about 0.7 s on the #209 layered
20 MP document with effects) and loaded new textures for each.
A new snapshot that differs from the previous one only in its selection
keeps the thumbnails (`same_but_selection`: the old document with the
new selection compared to the new one; tiles compare by pointer first).
A real rebuild updates the existing textures with `set` instead of
allocating new ones. The new test fails on the old code: a marquee
and Deselect no longer upload anything, and a fill rebuilds into the
same textures.
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
Session::apply_prefs set the shared text engine's missing-glyph
protection through photocraft_text::shared(), which blocks until the
system font scan that launch starts in the background is done. It runs
on the UI thread at launch and on every preferences change, so the first
frame waited for the rest of the scan: 248–257 ms on this machine
(release, launch order replayed), more with many fonts installed.
set_shared_missing_glyph_protection applies it at once when the engine
is built, and otherwise from one helper thread once the scan finishes.
Both read the newest setting under the engine's lock, so a late change
is never overwritten by an older one. Measured the same way: 0.1 ms.
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
* Checkboxes: a pressed box shows it, as in Photoshop (#4476)
Pressing a checkbox, its label or a Brush Settings section row gave no
feedback until the release. While pressed, an unchecked box now darkens
a little and a checked one turns a little less opaque.
widgets::check_box paints the box for both the shared checkbox and the
Brush Settings section list (which drew its own). The section boxes now
get the field fill like every other checkbox.
Tests: section_boxes_show_a_pressed_state,
checkboxes_show_a_pressed_state_from_their_label.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Checkboxes: checkbox-only Brush Settings rows show the pressed state too
main made the rows without settings (Wet Edges, Build-up, ...) one
checkbox across the row; a press anywhere on such a row presses its box.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
Third-party action sets that load since 3204231 now play. Four TrueGrit
Texture Supply sets (Debaser, Distress Press, Halftone Zine Machine,
Transparency) go from 223 to 3 unmapped steps of 533 and play end to end.
- .atn import, 108 -> 112 events: Select and Mask (refineSelectionEdge),
Filter Gallery Spatter (GEfc), Ripple, Crystallize; targeting the layer
mask or composite channel, loading the targeted channel, Step
Backward/Forward, removeFromSelection (it silently imported as a plain
select), Pattern Fill layers (by name), nudges in points, Layer From
Background, Grayscale/Bitmap mode objects, Image Size by resolution.
- Engine: layer.translate units pt; layer.select mode remove;
select.loadSelection channel targeted; image.imageSize keepPrintSize;
layer.new.layerFromBackground ifPresent/select; pattern.import data.
New from Clipboard keeps the copied document's resolution (was 72 ppi).
- Filters: Spatter's Smoothness smooths the displacement (torn but
attached edges, as Photoshop's); Crystallize fills a cell from its point
instead of the average, so masks keep hard edges.
- UI: .pat opens/drops into the Patterns panel (no UI import existed).
- Docs: gaps G11, file-format and target-app parity.
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
* fix(ui): expose custom paper dimensions in Print Settings
* fix(i18n): keep custom paper labels away from catalog tails
* Custom print paper: Serbian rows; drop the env-gated capture test
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: zenjabba <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
* Brush presets: remember what the New Brush Preset dialog captured (#4490)
* Brushes: a New Brush Preset dialog for every way of making a preset (#4490)
* Brushes: the New Brush Preset strings in the Serbian catalog (#4490)
---------
Co-authored-by: Brandon Thomas <[email protected]>
* Fix: Preview pixelation in Sharpen filters by using full resolution
Sharpen filters (Sharpen, Sharpen More, Sharpen Edges, Unsharp Mask, Smart Sharpen) now preview at full resolution instead of using a downsampled proxy. This fixes the pixelated appearance when enabling preview.
Sharpen algorithms work by detecting and enhancing edges. On a downsampled proxy image with fewer edges and details, the result appears pixelated or blocky. By previewing at full resolution (k=1), users now see an accurate preview that matches the final result.
Fixes#3929
* Fix: Preview issues in edge-detection filters (High Pass, Find Edges, Emboss)
High Pass, Find Edges, and Emboss filters now preview at full resolution instead of using a downsampled proxy. This fixes pixelated or inaccurate previews when enabling preview.
These filters work by detecting and enhancing edges:
- High Pass: extracts detail/edge information
- Find Edges: performs edge detection
- Emboss: creates 3D effect by detecting edges
On a downsampled proxy image with fewer edges and details, the result appears pixelated or blocky. By previewing at full resolution (k=1), users now see an accurate preview.
Similar to the Sharpen filter fix (#3929).
* Fix: Preview issues in morphological and motion blur filters
Median, Maximum, Minimum, and Motion Blur filters now preview at full resolution instead of using a downsampled proxy. This fixes pixelated or inaccurate previews.
**Median** - Similar to Dust & Scratches, removes noise by replacing pixels with median of neighborhood. Downsampling averages away the speckles it's designed to remove.
**Maximum/Minimum** - Morphological operations (dilate/erode) that expand or shrink features by one pixel. On a proxy, these features become fractional pixels, distorting the preview.
**Motion Blur** - Creates directional blur streaks. Pixel-sized distance parameter (1..2000 px) needs full resolution for accurate preview.
Like sharpen and edge-detection filters, these algorithms depend on pixel-level detail and need full resolution to preview accurately.
* Fix: Preview issues in despeckle and pixel-based blur filters
Despeckle, Box Blur, and Surface Blur now preview at full resolution instead of using a downsampled proxy. This fixes pixelated or inaccurate previews.
**Despeckle** - Removes one-pixel speckles/noise. Like Dust & Scratches, it works on per-pixel features that downsampling smooths away.
**Box Blur** - Creates rectangular blur artifacts with fixed pixel-sized radius (1..2000 px). On downsampled images, these rectangular patterns distort because the radius becomes a fraction of a pixel.
**Surface Blur** - Selective blur that preserves edges while smoothing flat areas. On downsampled proxy, edge preservation algorithm may not work correctly since edges have been blurred together.
All three algorithms depend on precise pixel-level operations and produce inaccurate results on downsampled images.
* Fix: Correct preview_factor for fixed-kernel sharpen and edge-detection filters
Remove parameterized filters (Unsharp Mask, Smart Sharpen, High Pass, Median, Maximum, Minimum, Motion Blur, Box Blur, Surface Blur) from FULL_RESOLUTION_PREVIEW.
These filters have pixel-sized parameters (radius, distance) that preview_factor already scales correctly. Forcing them to k=1 makes previews of large blurs on 24-36 MP documents unnecessarily slow without changing the result.
Keep only fixed-kernel filters that have no size parameter:
- Sharpen, Sharpen More, Sharpen Edges (one-pixel unsharp mask kernel)
- Find Edges (one-pixel edge detection kernel)
- Emboss (one-pixel edge-based emboss kernel)
- Despeckle (one-pixel speckle removal kernel)
These use fixed one-pixel kernels with no size parameter, so a downsampled proxy changes their look: edges average away, making sharpening and edge detection invisible. These must preview at full resolution (#3929).
Add tests to verify preview_factor returns 1 for these 6 filters.
Update doc comment to explain why each filter is in FULL_RESOLUTION_PREVIEW.
---------
Co-authored-by: AvrazAkraye <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
* feat(vector): add shape from selection with simplify tolerance (#214)
- Add tolerance and simplifyTolerance parameter (clamped 0.5..10.0 px) to select.convertToShape.
- Wire select.convertToShape in filter_dialog::has_dialog to prompt for tolerance.
- Add tests for knot count tolerance scaling and selection requirement.
* rustfmt
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
The engine already took a source layer and Add/Subtract scale and offset,
but the dialog only offered the merged image and no way to set Scale or
Offset, so the frequency separation recipe (Subtract, Scale 2, Offset 128)
couldn't be done from the menu.
Fixes#3965
Co-authored-by: Fabio Lussi <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
Windows and Linux already marked Yes and No. Cancel had no letter, so the prompt now shows (C)ancel and C cancels. macOS still hides the letters and keeps the shortcut. (#4582)
Co-authored-by: Brandon Thomas <[email protected]>
Context menus inherited a 6 pt item gap that belonged to neither button, so a drag across Merge Down or an adjustment list fell into dead space. Popup menus now stack their rows, and Pro rows use a square hover. (#4602)
Co-authored-by: Brandon Thomas <[email protected]>
Effect eyes sat in the same column as layer eyes, so a long list read as extra layers. The eye, its hit target and the label now sit to the right of that column. (#4572)
Co-authored-by: Brandon Thomas <[email protected]>
The contrast glyph put a smaller semicircle inside the ring, which read as concentric rings on a layer thumbnail. It is now one circle with a filled half. (#4561)
Co-authored-by: Brandon Thomas <[email protected]>
Tutar is a sum of money. Filter and adjustment Amount strings now use Miktar, the word for a quantity. (#4350)
Co-authored-by: Brandon Thomas <[email protected]>
The thin scrollbar faded its handle to the same opacity as the track, so scrolling a menu showed a bar with no position. The handle stays opaque and the resting bar is slightly wider. (#4611)
Co-authored-by: Brandon Thomas <[email protected]>
A negative Parent, font or normal-sheet index was cast to a huge usize, which skipped the name fallback and could hide the text. Those indexes now have to fit in a usize, and an infinite dpi uses 72. (#4429)
Co-authored-by: Brandon Thomas <[email protected]>
Feather r50 on 24MP was at ~81ms kernel / ~130ms scenario against the
100ms P27 budget. The cosine-series line() allocated a fresh
n-usize run table per row (thousands of ~48KB Vecs on a 24MP mask).
Reuse one buffer per worker thread via a thread-local; no arithmetic
touched, oracle tests (Photoshop-mapped + direct-Gaussian <=1/255)
prove identical output.
bench_24mp (24MP ellipse, best of 5, x86-64 release):
feather r50: 80.8 -> 70.7ms (-12.5%)
feather r250: 144.1 -> 133.6ms (-7.3%)
feather r1000: 301.7 -> 287.7ms (-4.6%)
Full perf_scenarios P27 with patch: p50 119.3ms (was over by ~30ms,
now over by ~19ms); remaining gap is session/snapshot overhead,
follow-up.
Co-authored-by: Brandon Thomas <[email protected]>
As in Photoshop, a Channels panel row can be dragged onto the footer's
buttons: New Channel duplicates it (the layer mask's row as
"<layer> Mask copy"), Delete deletes an alpha or spot channel, and Load
Channel as Selection loads it. The layer mask's row on Delete asks
"Apply mask to layer before removing?", like its Layers thumbnail.
Colour channels can't be deleted, so Delete ignores them. The dragged
name follows the pointer and the button under it is highlighted.
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
* Free Transform: H and V skew fields in the options bar
Photoshop's Free Transform options bar has H and V skew fields after the
angle; ours stopped at the angle (ui-parity: "Options bar H/V skew
fields | missing", gaps G5).
The bar now reads the box as Photoshop does: the frame scaled (W, H),
skewed (H, V) and rotated about the reference point (`Parts`). Typing a
skew leans the box about the reference point and keeps W, H and the
angle. A box can be read with any V skew, so the session keeps the typed
one (`TransformSession::skew_v`, serde default). Skews stop at ±89°.
A sheared box used to read as taller (H = 100/cos h %); it now reads as
an H skew with H at 100 %. Readouts without skew are unchanged, flips
included.
Docs: ui-parity row done, G5 and target-app-parity notes updated.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* i18n(sr): translate the Free Transform skew field tooltips
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
* fix(picker): the screen color picker no longer crashes on DX12 or traps the overlay on Windows (#4443)
Two Windows-only faults in the Pick screen color window:
- Created always-on-top, the monitor-sized picker window makes the DX12
backend fail swapchain creation (0x887A0001, DXGI_ERROR_INVALID_CALL);
wgpu then panics in Surface::get_current_texture_view. The window is now
created at the normal level and raised once its first frame is painted.
- As a deferred viewport the picker was painted on its own, and the main
window stopped being redrawn while it was up on Windows (OpenGL). A click
or Esc only stored an answer that the main window's tick() never took, so
the overlay could not be left. It is now an immediate viewport, painted
inside the main window's frame.
Verified on Windows 11 / Intel UHD 630: DX12 (default) opens, picks and
cancels; OpenGL picks and cancels; macOS still picks and cancels. Adds a
test that the picker viewport is not built always-on-top.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
* Screen picker: raise the window level once per picker window, not every frame
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Claude Sonnet 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
* Status bar: wrap fullscreen zoom scrubbing at horizontal edges (#3344)
* Status bar: preserve keyboard editing and handle fullscreen edge crossings
* widgets: initialise the new FieldOpts field after merging main
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Opening a file whose type uses a font that isn't installed said nothing
in the desktop app, so the text silently rendered in a fallback face.
The CLI and automation already reported it; Photoshop warns on open.
The open notice now lists each missing family, with the engine's
existing warning text (type_extra_cmds::missing_font_warnings), for both
the synchronous open and the background open the desktop app uses.
Tests: opening_type_in_a_missing_font_warns,
background_open_warns_about_missing_fonts.
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
* Make Edit Transform Rotate and Skew use their own handle drags.
Rotate and Scale both started Free Transform, so a corner drag scaled the box. Skew only remapped edge handles, so corners still scaled. Give Rotate its own mode that turns the box from handles, and skew a corner along its adjacent edge (#3941).
* Rotate mode: fix the Skew doc comment and rustfmt
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
The Serbian catalog (#4436) and the Soft themes (#4378) landed together, so
sr.tsv lacked their four strings and the ui-egui i18n completeness tests failed.
Co-authored-by: Claude Opus 5.5 <[email protected]>
* perf: reuse sampled composite across channel visibility toggles
* test: borrow full composite for sampled-grid oracle
* test: compare the entire channel view after invalidation
* style: keep channel cache construction readable
* fix: format channel test and share band renderer
* fix: invalidate sampled channel composite after edits and previews
* refactor: keep channel rendering optimization isolated from compositor
* test: cover both partial-damage and full channel redraw paths
---------
Co-authored-by: Brandon Thomas <[email protected]>
The preview treated every drag, including Alt plus the right button, as a stroke. That gesture now sets the diameter and density, and a plain left drag still paints. Fixes#4702.
Co-authored-by: Brandon Thomas <[email protected]>