Repository navigation
Image Rotation, Unsharp Mask and High Pass, Align and Distribute, layer commands, Edit › Stroke - #2
Merged
Merged
Conversation
The editor had no sharpening filter. Unsharp Mask (Amount, Radius, Threshold) and High Pass (Radius) are Photoshop's standard ones: each blurs the layer with its edge held, then a C kernel in AdjustPixels.c pushes every channel away from the blur, or keeps only the detail about middle gray. Both preview live, keep to the selection and the layer's size, and apply as one undo step through the existing filter panel. Ported from upstream PR robbietilton#196, with a test that the selection limits both filters and the layer keeps its size. robbietilton#196 Co-authored-by: Rezaul Arif <rezaulkarimarif.ctG@gmail.com>
Lining layers up is a compositing staple the editor lacked. Selected layers now align by their edges or centers to the selection, to the canvas when one thing is selected, or to each other, and three or more distribute by centers or spacing, from the Layer menu and the Move tool's bar. A selected folder moves as one, moves are whole pixels and each command is one undo step. Upstream measured each layer's transform box. Photoshop measures the pixels a layer shows, so a layer with transparent padding would line up by its padding instead of its content. Each layer is measured by its non-transparent pixels, mapped through its transform; a layer showing nothing is left alone, and a hidden layer inside a selected folder moves with it without counting toward its box. Ported from upstream PR robbietilton#191, with tests for padded, flipped, empty and hidden layers. robbietilton#191 Co-authored-by: Rezaul Arif <rezaulkarimarif.ctG@gmail.com>
…yle commands Upstream took the masks, Auto Select, tabs and Ungroup parts of PR robbietilton#163 but left out its layer commands to keep menus short. They are core compositing commands Photoshop users reach for, so they come over now, each one undo step: - Apply Layer Mask bakes an enabled mask into the layer's own pixels at their resolution (not for folders, adjustments, live text or shapes). - Merge Visible combines everything showing into one layer at the top of the visible stack and leaves hidden layers alone; Flatten Image combines it into one "Background" layer and drops hidden ones. - Copy, Paste and Clear Layer Style act on a layer's effects, with one copied style for the whole app. - Show/Hide All Other Layers solos a layer, keeping its folders showing. - A layer's pixels or mask can be intersected with the selection, as well as added to and subtracted from it. They are in the Layer menu and the Layers panel's right-click menu. A right-click on a thumbnail also offers Select, Add, Subtract and Intersect for its pixels or mask, and Cmd-Shift-Option-click on a thumbnail intersects, as in Photoshop. The per-part right-click menus of the original commit are not ported; the fork keeps one row menu. Ported from commit aea6e20 of closed upstream PR robbietilton#163, with its tests and new ones for the context menu and intersecting with a mask. robbietilton#163 Co-authored-by: cheyras <38292020+cheyras@users.noreply.github.com>
Stroking a selection's outline is a standard Photoshop command the editor lacked (upstream issue robbietilton#177). Edit › Stroke… asks for a width, the foreground or background color, an opacity and Inside, Center or Outside, then paints the line on the active layer or its mask as one undo step. The line isn't clipped to the selection, and only the tiles it passes through are touched, so a thin stroke round a large selection stays cheap. Upstream painted a mask with the color's red channel. A mask now takes the color's luminance (Rec. 709 weights), so a color never paints as its red channel alone; the mask palette is black and white today, so nothing changes there yet. Ported from upstream PR robbietilton#190, with tests for stroking a mask and for the luminance. robbietilton#177 robbietilton#190 Co-authored-by: Rezaul Arif <rezaulkarimarif.ctG@gmail.com>
The editor could flip the canvas but not turn it (upstream issue robbietilton#212). Image › Image Rotation turns the whole document as one undo step: layer pixels and masks are turned whole with vImage off the main thread, so nothing is resampled and turning back gives the same bytes; the selection and guides turn with them, and a quarter turn swaps the canvas's width and height. Live text and shapes keep their settings and turn by their angle, so they stay editable. Undo and redo of a size change now refit the view. Rotation rebuilds the document in its own code path, from the layers as they are, not from a project snapshot, so it keeps layer effects (dropped by snapshot rebuilds until the effects fix lands); a test checks that effects, live text and shapes survive a turn. Ported from upstream PR robbietilton#195. robbietilton#212 robbietilton#195 Co-authored-by: Rezaul Arif <rezaulkarimarif.ctG@gmail.com>
The Width slider stepped by one pixel over 1–250, and on macOS a stepped slider draws a tick for every step, which showed as a dashed line under it. Its binding already rounds to whole pixels, so the step is dropped.
Ten icon buttons after Flip V ran past the edge of the bar's scroll view in a laptop-width window (1512 pt), leaving all but two scrolled out of sight with no indicator. One Align and Distribute pull-down, with the commands in two sections, fits beside Flip V; the Layer menu still lists them all.
Plans, notes, verification and final summaries for Image Rotation, Unsharp Mask and High Pass, Align and Distribute, the layer commands and Edit › Stroke, with before/after images in backlog/assets.
itsjavi
added a commit
that referenced
this pull request
Oct 7, 2026
…shortcuts-and-workflow Conflicts: README (custom crop ratios next to Image Rotation) and the Layer menu (Show/Hide Layer keeps its assignable shortcut beside #2's Show/Hide All Other Layers). The menu commands #1 and #2 added had no way to take a key, which this PR promises for every menu command, so they're now in More Menu Commands: File › Export As…, Edit › Stroke…, Image › Image Rotation's three items, and Layer › Apply Layer Mask, Show or Hide All Other Layers, Merge Visible, Flatten Image, Align, Distribute and Layer Style. Unsharp Mask and High Pass were already covered by the generated Filter list.
itsjavi
added a commit
that referenced
this pull request
Oct 8, 2026
…into feat/cli Conflict: Package.swift, the app target's dependencies (LaminaAutomation from this PR, libwebp from #1); both kept. #1 replaced the JPEG-only export API (JPEGOptions, ImageExporter.jpeg) with ExportOptions and encode(_:options:), so export-document's JPEG branch uses those now. The command still offers PNG and JPEG; #1's other formats could join it later.
itsjavi
added a commit
that referenced
this pull request
Oct 8, 2026
…into fix/upstream-fixes Conflict: MaskTracing. This PR made tracing throw when an outline is too detailed and reported that from Load Mask/Layer Selection; #2 moved the tracing into two helpers shared with its new Intersect commands. The helpers now throw, and all four commands report the error the same way (Load beeps when there's nothing to trace, as before).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Five Photoshop staples ported from upstream, each adapted where the backlog asked:
Each is one undo step. One commit per task, plus two small follow-ups (the Stroke sheet's slider ticks, and fitting Align and Distribute into the Move tool's bar) and the backlog records with screenshots.
Merge note: merge this after the upstream-fixes PR (TASK-8, effects dropped when a document is rebuilt from a snapshot). Rotation doesn't go through that rebuild (it turns the layers as they are), and a test checks that effects survive a turn, but TASK-14 depends on TASK-8 in the backlog.
Image › Image Rotation (TASK-14)
Ported from upstream PR robbietilton#195. Layer and mask pixels are turned whole with vImage, off the main thread, so nothing is resampled and four quarter turns give back the same bytes. The selection and guides turn too, and a quarter turn swaps the canvas's width and height. Live text and shapes keep their settings and turn by their angle, so they stay editable. Undo and redo of a size change now refit the view.
Rotation builds the turned document from the layers as they are, not from a project snapshot, so layer effects, text and shape settings carry over. Effects keep their angle, as Photoshop's Global Light does: a drop shadow still falls downward after the turn. New test:
effectsLiveTextAndShapesSurviveATurn.Filter › Unsharp Mask and High Pass (TASK-15)
Ported from upstream PR robbietilton#196 as-is. Unsharp Mask (Amount, Radius, Threshold) and High Pass (Radius) blur the layer with its edge held, then C kernels in
AdjustPixels.cpush each channel away from the blur, or keep only the detail around middle gray. They preview live in the existing filter panel and keep to the selection and the layer's size. The Filter menu lists them throughFilterKind.allCases. New test: through the session with a selection, one undo step, same size and place, pixels outside the selection unchanged.Align and Distribute (TASK-16)
Ported from upstream PR robbietilton#191, with one change. Upstream measured each layer's transform box, but Photoshop measures the pixels a layer shows, so a layer with transparent padding would line up by its padding. Each layer is now measured by its non-transparent pixels (
brush_alpha_bounds), mapped through its transform, so flips and rotations count. A layer showing nothing is left alone. A hidden layer inside a selected folder moves with the folder but doesn't count toward its box. Moves are whole pixels.Align works on edges or centers, against the selection, the canvas when one thing is selected, or the selected layers' combined box. Distribute works on centers or spacing, for three or more layers. Both are in the Layer menu (Align and Distribute submenus). In the Move tool's bar they're one pull-down, not upstream's ten icons: at a 1512 pt window (a 14" laptop) eight of the ten were scrolled out of sight past the bar's edge.
Layer commands (TASK-17)
Ported from commit aea6e20 of closed upstream PR robbietilton#163, taking only these commands. Ungroup Layers was already in the fork.
All of these are in the Layer menu and in the Layers panel's right-click menu. A right-click on a thumbnail also offers Select / Add to / Subtract from / Intersect with Selection for its pixels or mask, and Cmd-Shift-Option-click on a thumbnail intersects, as in Photoshop. The original commit's separate menus for each part of a row are not ported; the fork keeps its one row menu and adds a selection section when the click lands on a thumbnail.
Edit › Stroke (TASK-19)
Ported from upstream PR robbietilton#190 (issue robbietilton#177). The sheet asks for width, foreground or background color, opacity, and Inside, Center or Outside, then draws a line along the selection's outline on the layer or its mask. The line isn't clipped to the selection, and only the tiles it crosses are touched. Upstream painted masks with the color's red channel; they now take its luminance (Rec. 709). The mask palette is black and white today, so nothing visible changes yet. Also dropped the Width slider's step, which on macOS drew a tick for every pixel under it (the binding already rounds).
Testing
All with
swift build --disable-keychain --build-tests, thenswift test --disable-keychain --skip-build --filter …:SharpenFilterTests: 5 passed (4 ported, 1 new)LayerAlignTests: 8 passed (5 ported, 3 new: padding and flips, empty layer, hidden layer in a folder)LayerCommandTests|LayerTests|LayerMaskTests: passed (ported tests plus new context-menu, thumbnail-menu and mask-intersect tests). The filter also matched TiledLayerTests, AdjustmentEditorTests and AdjustmentLayerTests.SelectionStrokeTests: 5 passed (3 ported, plus a mask stroke and luminance);SelectionEditTests(sharespaintCanvas): 19 passedCanvasRotationTests: 5 passed (4 ported, 1 new);CanvasSizeTests|ImageSizeTests|HistoryTests(therestorechange): 14 passedSharpenFilterTests|LayerAlignTests|LayerCommandTests|LayerTests|LayerMaskTests|SelectionStrokeTests|SelectionEditTests|CanvasRotationTests: 101 tests in 11 suites passedThe full suite was not run.
Screenshots
Document images were rendered offscreen through the app's export path, from sessions driven by the real commands (a throwaway test, not committed). Panels and sheets were rendered offscreen with
NSHostingView. The Move tool bar shots are real windows: the baseline app built from main, and this branch'smake devbuild, each opened on the same fixture image. Fixtures are synthetic.Image Rotation (TASK-14)
Fixture: a soft landscape, a card with Drop Shadow, an ellipse shape with Stroke, and live text "Rotate" with Stroke.
After: the canvas is 400 × 600, the shadow still falls downward, and the text and shape keep their strokes and stay live.
Unsharp Mask and High Pass (TASK-15)
Detail, 3× crop, before and after Unsharp Mask:
The Unsharp Mask panel (new; rendered offscreen during a live filter edit):
Align and Distribute (TASK-16)
Align Left Edges on three layers, each clear except for one shape placed off-center in its box. Dashed lines show each layer's bounds; the pink line marks the aligned edge.
After: the shapes' left edges meet at x = 170 while their boxes don't. Measured by the transform box, as upstream did, the boxes would line up instead.
Move tool bar, real window with the fixture open. Before (main):


After (this branch): the Align and Distribute pull-down after Flip V:
Layer commands (TASK-17)
Layers panel (the real table, rendered offscreen) with a photo, Red, a hidden layer and Green.
Merge Visible names the result after the topmost visible layer and leaves the hidden one alone; Flatten Image leaves one Background layer.
Edit › Stroke (TASK-19)
The Stroke sheet (new; rendered offscreen):
Manual checks still needed
Menus and sheets over a window can't be captured here:
Upstream (robbietilton/Compositor):