Commit Graph
2767 Commits
Author SHA1 Message Date
Scott MacdonaldandClaude Opus 5.5 2cb396d154 fix(engine): Displace smart filter ignores its mapPath displacement map (#3676)
A Displace applied to a smart object is recorded as a smart filter and
rendered by apply_smart_filters -> filters::apply_filter_to_surface, both
when it is added and on every later re-render (replace contents, toggling
a filter, transforms). That path built FilterParams with params_for alone,
which leaves Displace's map as None; only run_filter resolved `mapPath`
(through filters_ext::resolve, which needs the session). With no map,
algo's displace returns the source unchanged, so the smart filter was a
silent no-op.

apply_filter_to_surface now resolves `mapPath` through the same file read
and import the direct command uses (filters_ext::map_from_path, shared by
resolve and the new session-free resolve_map_path). A map that can no
longer be read on re-render leaves the pixels alone, as for an unknown
filter; the command still refuses an unreadable map when the filter is
added. `mapDocument` needs the session and stays unresolved on re-render.

Regression test: smart_cmds::tests::
displace_smart_filter_reads_its_map_path_on_re_render (8/16/32-bit).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 06:28:33 -04:00
Valentin KleinandClaude Opus 5.5 64a02107ba PSD: cap descriptor list/object preallocation from untrusted counts (#3636) (#3699)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 06:28:18 -04:00
Valentin KleinandClaude Opus 5.5 16341720c0 PSD: bound the zero padding of undersized pattern channels (#3652) (#3686)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 06:28:14 -04:00
Valentin KleinandClaude Opus 5.5 7c77143b97 PSD: don't preallocate knots from untrusted path length counts (#3635) (#3654)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 06:28:09 -04:00
Valentin KleinandClaude Opus 5.5 04396dd85f PSD: composite_rgba8 checks the output budget and converts planes fallibly (#3639) (#3651)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 06:27:57 -04:00
Brandon ThomasandClaude Opus 5.5 27f56eb762 feat(compose): Lab documents evaluate Invert, Posterize, Threshold, Brightness/Contrast and Gradient Map on L, a, b (#4038)
In Lab documents Photoshop evaluates these adjustments on the Lab channels,
not on the screen RGB colour:

- Invert and Posterize act on L, a and b alike (normalised L/100,
  (a+128)/255, (b+128)/255; Invert is n -> 1 - n).
- Brightness/Contrast (modern and legacy) applies its tone curve to L only;
  a and b are unchanged.
- Threshold splits on L alone and outputs neutral black or white.
- Gradient Map is driven by L (Reverse uses 1 - L).

Adjustment layers (CPU compositor) and destructive Image > Adjustments both
go through the new `adjust::apply_in`; the GPU compositor defers these
adjustments to the CPU in Lab documents. Levels and Curves keep their Lab
records; other adjustments stay on the screen colour.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 06:20:44 -04:00
zenjabbaandzenjabba ce16e100b0 Expose WebP and optional Batch export quality (#3956)
Co-authored-by: zenjabba <679864+zenjabba@users.noreply.github.com>
2026-10-12 06:20:16 -04:00
McClabe be877c27d0 fix(ui): keep filter jobs polling on idle Linux (#3961) 2026-10-12 06:20:00 -04:00
Nigel Ihza Firdausy 6ca7e60cb0 fix(ui): make checkbox labels nonselectable with pointer cursors (#4002) 2026-10-12 06:19:44 -04:00
Nigel Ihza Firdausy 73520f8681 fix(ui): Open layer color choices from the visibility icon (#4001)
* fix(ui): offer label colors from the layer eye icon

* fix(ui): offer label colors from the layer eye icon

* test(ui): cover layer eye color context menu

* style(ui): keep extracted color menu indentation consistent
2026-10-12 06:19:28 -04:00
MarekSpodBiedry db509dc4d9 fix(ui): show a paint-bucket cursor for the Paint Bucket tool (#3968) (#3989) 2026-10-12 06:19:13 -04:00
Vikram Bhaskaran ad3417dc05 Dismiss Home when the document count changes. (#3984)
Chrome stored the document count when Home opened so File New could leave the overlay. Closing a tab that restored that count re-opened Home and hid remaining tabs (#3981). Clear the stored count as soon as it no longer matches, then keep Home dismissed until the user opens it again.
2026-10-12 06:18:58 -04:00
Brandon ThomasandClaude Opus 5.5 0d6e658e73 PSD: rasterized fill and smart layers stay rasterized after save and reopen (#4034)
* fix(psd): rasterized fill and smart layers stay rasterized after save and reopen

Layer > Rasterize > Fill Content / Smart Object swapped the layer's content
to pixels but kept the fill (SoCo/GdFl/PtFl) or placed-layer (SoLd/PlLd/SoLE)
blocks read from the PSD. Written back, they made the reopened file a fill
layer or smart object again and discarded the pixels. Photoshop writes none
of them for a pixel layer.

- Rasterize drops those blocks from the layer.
- The PSD exporter never writes fill, placed, type (TySh), shape-content
  (vscg/vstk), adjustment or CgEd blocks for a raster layer, whichever
  command made it one; a vector mask (vmsk/vsms with its vogk) stays.

Tests: synthetic fill + smart object rasterize/save/reopen, the exporter
backstop with stale blocks, and Photoshop-authored corpus files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(psd): keep a pixel layer's vstk when it has no vector mask, as Photoshop does

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 06:11:33 -04:00
Brandon ThomasandClaude Opus 5.5 6f1c4481bf fix(psd): type layer bytes match Photoshop (TySh bounds, AntiAlias, version info) (#4041)
- TySh: the four values after the warp descriptor are big-endian f32, not
  i32. Written as Photoshop writes them: zeros for unwarped type, the
  text-space bounds for warped type (corpus: warp-arc-50.psd, unwarped
  text oracles).
- EngineData AntiAlias numbers as Photoshop writes them, on write and on
  read (when there is no AntA descriptor): 0 None, 1 Crisp, 2 Strong,
  3 Smooth, 4 Sharp (corpus text/antialias-*.psd). Platform modes, which
  no corpus file confirms, write Smooth and keep their exact mode in AntA.
- Resource 1057: writer "Adobe Photoshop", reader "Adobe Photoshop 2026",
  no NUL terminator, byte for byte what all 258 Photoshop 2026 oracles
  write.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 06:06:46 -04:00
5bc66094f8 fix(ui): allow selecting shapes and editing color with path selection (#3915) (#3932)
* fix(ui): allow selecting shapes and editing color with path selection (#3915)

- In vector_ui.rs, implement pick_shape_at to hit-test visible shape layers by path, surface pixels, or live shape geometry.
- In path_selection_finish and canvas ToolEvent::Down, select the clicked shape layer under the pointer when using Path Selection.
- In direct_select.rs, include visible shape layers in candidate hit tests and sync layer.select on press.
- In options_bar, display Fill swatch/checkbox, Stroke swatch/width/style, and corner radius when a shape is selected with Path Selection or Direct Selection, and dispatch shape.edit.
- Add tests in path_selection_tests.rs and vector_ui.rs verifying shape selection on click and options bar editing.

Fixes storytold/photocraft#3915

* Collapse nested ifs for clippy (collapsible_if)

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>
2026-10-12 06:05:15 -04:00
Scott C Hughes 24bd7533fc feat(ui): the generic filter dialogs get a Photoshop-style preview window (#3769) (#3877)
Photoshop shows a preview of the filtered result inside its filter dialogs; ours only had the parameter widgets, with the live result on the canvas behind the dialog. The generic schema dialog now draws a preview window from the same downsampled proxy the canvas already computes, with - / + zoom steps and a percentage readout, drag to pan, a click to centre on a document point, and press-and-hold to compare with the unfiltered image. It sits under the parameters (reserving its height so a small window still fits OK/Cancel); the Preview checkbox still toggles the on-canvas result, unchanged.

Tests: cargo fmt --all -- --check; cargo test -p photocraft-ui-egui --lib (only the pre-existing Czech cs.tsv duplicate-key test fails); cargo clippy -p photocraft-ui-egui --all-targets -- -A clippy::nonminimal_bool -A clippy::collapsible_match -D warnings; cargo xtask wasm; a snapshot of the Emboss dialog shows the window and its zoom controls.
2026-10-12 06:05:02 -04:00
Fabian GhenofandClaude Opus 5.5 83f4452bf2 Layer › Export As exports every selected layer or artboard (#3629) (#3909)
With several layers selected, Export As was greyed: its dialog exported only
the active layer. Someone with 20+ artboards (ads, Steam store capsules) had
to export them one at a time.

Export As now opens the same dialog for the selection ("Export As: N
layers", the preview showing the first) and, on Export, asks for one
destination and writes one file per layer with the dialog's format, quality,
transparency and scale, each at its own size. Files are named as Quick
Export as PNG names a selection's: the chosen path, then numbered siblings
named after each layer, now in the chosen format. The layers are captured
when Export is pressed.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 06:04:35 -04:00
Scott C Hughes 19e51cf0ad refactor(ui): unify plain text inputs on one text_field widget (#3789) (#3896)
Plain text fields (Preferences search and hex, the Color Picker hex, and the brush search fields) each drew egui's default TextEdit frame, so they differed in padding and border and showed egui's white focus ring (its selection.stroke). Add widgets::text_field, which draws the same box as a value_field (field fill, field_border, radius_sm) with frame NONE, a consistent inner padding, no focus outline, and a scoped, non-blue text-selection colour (grey in the monochrome Pro themes, the theme accent elsewhere). Migrate those fields to it; numeric fields already use value_field.

Tests: cargo fmt --all -- --check; cargo test -p photocraft-ui-egui --lib; cargo clippy -p photocraft-ui-egui --all-targets -- -A clippy::nonminimal_bool -A clippy::collapsible_match -D warnings; cargo xtask wasm
2026-10-12 06:04:23 -04:00
coolvitto c0fa2269d2 Update Japanese translations (#3892)
Improved readability and several translation corrections.
2026-10-12 05:57:45 -04:00
Brandon ThomasandClaude Opus 5.5 32552a61a1 fix(liquify): dab spacing runs on along the whole stroke, so pointer speed no longer changes the warp (#2606) (#4047)
Liquify dabs were spaced a fifth of the brush radius apart and restarted
at every pointer event, so a fast mouse (few far-apart events) and a slow
one (many close ones) warped the same path differently, and a slow drag
piled up one dab per event. Dabs now fall every twentieth of the radius
by distance travelled along the whole stroke, carried from one segment
to the next, as in Photoshop: any sampling of a path gives the same
field. The stationary tools' per-dab strength is scaled to the new
spacing so their effect per distance travelled is unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 05:56:09 -04:00
Brandon ThomasandClaude Opus 5.5 f86517a029 fix(psd): write descriptor ids warp, list and time as length-4 strings (#4039)
A descriptor class or key id is a u32 length and bytes, where length 0
means a 4-byte code follows. Photoshop stores `warp`, `list` and `time`
as length-4 strings and treats the code form as a different key, so the
warps of new smart objects and type layers and regenerated layer comp
lists were not recognised. In the corpus, `warp` appears 741 times as a
string and never as a code, `list` 5/0, `time` 7/0.

`Id::new` now makes these three names string ids, as keys and class ids.
Reading is byte-based and accepts both forms; a code read from a file is
written back as a code.

Tests: exact bytes for each name as class and key, both forms read,
fresh placed-layer warp (SoLd/PlLd) and layer comp list bytes.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 05:50:36 -04:00
Brandon ThomasandClaude Opus 5.5 62346f7fb6 feat(save): File › Save keeps a JPEG's quality and a WebP's lossy/lossless encoding (#2399) (#4032)
Opening a JPEG now estimates the quality it was saved at from its quantisation
tables (the Annex K tables of ITU-T T.81 scaled by the IJG quality curve; exact
for such tables, the closest fit otherwise). Opening a WebP records whether it
is lossless (VP8L) or lossy (VP8), and a lossy one's quality from its base
quantiser index. The settings live on the document (`Document::file_encoding`,
saved in .pcraft) and a save that asks for no quality writes the file back the
same way, as Photoshop's Save does: File › Save, Save As and Save a Copy start
there instead of the fixed codec defaults (quality 90, WebP always lossless).
An explicit quality still wins; new documents and other formats keep the
defaults. Hostile or truncated DQT/VP8 headers give no estimate, never a panic.

Refs #2399

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 05:45:50 -04:00
Brandon ThomasandClaude Opus 5.5 3d0c819d46 Release prep: 0.8.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 05:44:37 -04:00
Brandon ThomasandClaude Opus 5.5 dd708f4ce4 feat(psd): Gradient Map stops in CMYK, Lab, Gray and HSB import and round-trip (#4033)
Photoshop writes a Gradient Map's colour stops in the document's own colour
space (CMYK, Lab, Grayscale) or HSB. PhotoCraft only read RGB stops, so such
layers imported as unsupported and did nothing.

- Decode every plain PSD colour space (RGB, HSB, CMYK, Lab, Grayscale) in
  `grdm` stops; CMYK converts through the document's own CMYK profile
  (photocraft-cms), Lab through D50 Lab. Book colours and unknown ids stay
  unsupported (kept verbatim).
- Saving keeps the file's own `grdm` block, so stops keep their colour space;
  toggling Reverse or Dither patches just those flags in place.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 05:43:27 -04:00
Reynold Hu e79d6571ba Actions: replay tests for crop, filters, selections and styled text on a second document (AUTO-219-4) (#3566)
* test(engine): a recorded edit run replays on a second document

* docs(scorecard): mark AUTO-219-4 done; the replay tests cover all four families
2026-10-12 05:36:39 -04:00
a1a7b17ee0 fix(engine): a PSD type layer names the installed font, not an invented one (#3556) (#3564)
* fix(engine): a PSD type layer names the installed font, not an invented one (#3556)

A type layer created with a font by family name stored no PostScript name, so PSD export
invented one ("<Family>-Regular", spaces removed). Photoshop looks type up by PostScript name,
so it showed the font as missing and substituted Myriad Pro as soon as the text was edited.

Resolve the family's installed face (matching weight/italic) from the font database when the
family, style, weight or italic is set, and store its real PostScript name; an explicit
postscriptName still wins. A family PhotoCraft does not have keeps no name, so the export falls
back as before.

Test: a_family_font_gets_the_installed_postscript_name; the existing named-face test now expects
the resolved name.

* Type tool test: a family change names the installed face (#3556)

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>
2026-10-12 05:32:03 -04:00
79cfa2b65a Guides: Shift-drag out of a ruler lands on the nearest ruler tick (#3770) (#3796)
As in Photoshop, holding Shift while dragging a new guide out of a ruler
snaps it to the tick marks the ruler draws (tenths of the labelled step),
in the current ruler unit and zoom. Without Shift the guide snaps to the
document, layers, selection and grid as before.

The tick spacing draw_rulers computed inline moves to ruler_scale, shared
by the renderer and the new snap_to_tick, so the snap always matches the
ticks on screen.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 05:22:58 -04:00
bade115202 Fail CLI run when recorded action playback fails (#3861)
Co-authored-by: jonathnwxh <258340642+jonathanwxh-cell@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-12 05:14:54 -04:00
Jakub 6d03075ed7 perf: draw marching ants as one mesh instead of a line shape per dash (#3846)
Every ants segment was a white line shape plus a black line shape per 4 px dash, and egui
tessellates each. A selection with many edges seen whole (a speckled selection at 30% zoom:
94,666 segments) cost 45.6 ms per idle frame, ten frames a second while the selection shows.

Horizontal and vertical segments now go into one egui::Mesh of 1 px colored quads, with the
same pixel-centre rounding, dash length and phase; the white base runs half a pixel past both
ends so corners join as the line caps did. Slanted segments (Content-Aware Move's transformed
outline) stay line shapes, which egui antialiases. A textured, repeating dash isn't possible:
egui-wgpu clamps texture coordinates.

Speckled selection, idle frame: 45.6 -> 12.0 ms at 30% zoom, 6.6 -> 4.5 ms at 125%
(RTX 3090, Vulkan; an idle frame with no selection is about 3 ms). Screenshots at 100% and
800% zoom are pixel-identical to the line shapes.
2026-10-12 05:14:41 -04:00
Sir Starch 5bdb12ec51 fix(canvas): keep transform preview visible while panning (#3610) (#3843) 2026-10-12 05:14:27 -04:00
Scott C Hughes ae0e5eb731 feat(ui): drag to reorder swatch groups and preset folders (#3658) (#3828)
Swatch groups and the shared preset-browser folders could only be clicked, so there was no way to put them in the order the user wants. Dragging a folder header now mirrors the dock's per-group tab reorder (#2272): an accent insertion line shows where it will land, and the order is stored by name in PresetUi (UI state, groupOrder) so it saves and reloads with the rest of the state, reading as leniently as tabOrder. Because the folders share one browser, Gradients, Patterns, Styles and Shapes get it too.

Tests: unit tests for the reorder and lenient/persisted order, plus an egui_kittest drag of a group header in the Swatches panel.
2026-10-12 05:14:17 -04:00
zenjabbaandzenjabba ad5fb53f68 Web: avoid shifting canvas during preset saves (#3805)
Co-authored-by: zenjabba <679864+zenjabba@users.noreply.github.com>
2026-10-12 05:14:07 -04:00
JGFandClaude Opus 5.5 a2cdc6c2d7 fix(ui): drag a document tab off the strip to float it in its own window (#3733) (#3785)
Dragging a document tab more than 24 pt above or below the tab strip now
tears it off into a separate OS window placed where it was dropped, so it
can be moved anywhere, other screens included (as in Photoshop). Sideways
drags still reorder. Both tab strips share the new tab_drag helper.

window.arrange.floatInWindow takes optional `document` and `position`
params (validated: bad values are errors, not panics); DocWindow gains a
serde-default `position`.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 05:13:48 -04:00
Valentin KleinandClaude Opus 5.5 9f49cae093 Move: with an unlinked layer mask targeted, the Move tool moves the mask alone (#3604) (#3611)
layer.translate takes target=mask: a layer whose mask is unlinked then shifts that mask only
(the shell adds the target when the Layers panel targets the mask), live preview included.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 05:04:13 -04:00
xsvm 6d21a69968 Brush: symmetric live strokes update only what changed (#3593) (#3605)
With painting symmetry on, every pointer event cloned and finished the stroke and each mirrored
pass, merged them again, composited the whole stroke and restored each pass's full bounds, so an
event cost grew with the area painted so far. StrokeRenderer::composite_union_live merges and
composites only the coverage tiles that changed (new dabs, the smoothing tails now and before),
leaving exactly the pixels the full merge gave; a mirror joining redraws every tile once.
2026-10-12 04:53:36 -04:00
4736a4bcbe Character panel: OpenType feature buttons (TYPE-218-5, #3752) (#3757)
The Character panel (and the Properties Character section) gains
Photoshop's row of eight OpenType buttons: Standard Ligatures, Contextual
Alternates, Discretionary Ligatures, Swash, Stylistic Alternates, Titling
Alternates, Ordinals and Fractions. Each shows pressed while its feature is
on at the selection (else the layer) and toggles it through the existing
type.openType.* command, on the same scope as the other Character controls
(the selected characters while typing, else every selected type layer).

Scorecard TYPE-218-5 stays partial: the Numerals and Stylistic Sets
submenus are still missing.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 04:52:23 -04:00
MrCloudy2andClaude Opus 5.5 8b21621671 Chisel bevels: Photoshop's facet slope and ramp ends (#3595)
Measured on Photoshop 27.11 (scripted: a 128 px square with one Bevel &
Emboss, light from the top at 30°, sampled pixel by pixel across its
edges; Chisel Hard and Soft, inner and outer, size 10 and 20, depth 100
and 300):

- A chisel facet's slope is depth × size / (2 (size + 1)): it rises to
  the same depth × size / 2 as a Smooth bevel, over size + 1 px.
  PhotoCraft gave it the full depth, about twice as steep (facet 217
  where Photoshop shows 188).
- Inner Hard's ramp runs from half a pixel beyond the edge to size + ½,
  so the facet is flat right up to the edge pixel; Inner Soft starts a
  pixel further out (a steeper edge pixel) and is not blurred; Outer
  Hard ends a pixel further out; Outer Soft is a pixel shorter with a
  steeper edge pixel. Outer chisel bevels also shade the pixel where
  their ramp reaches zero (it still slopes), as emboss styles already
  did.

All six inner and outer chisel cases are now within 1 level (they were
27 to 154 off); Smooth bevels are unchanged. Emboss and Pillow chisel
are not modelled yet (they keep the blur).

On the GPU: the same ramps in fs_mbevelh (Chisel Soft flag in p0.z), the
emboss-only blur, and the outer reach-past flag in p3.x; the 44 GPU
parity tests pass. Corpus: every Photoshop chisel oracle improves
(bevel-inner-chisel-hard 20.1 -> 6.5 % differing pixels, outer-chisel-
hard 13.6 -> 5.2 %, inner-chisel-soft 22.3 -> 9.3 %, emboss and pillow
chisel a few points); pass counts unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 04:43:01 -04:00
fabiodalez-dev dbf9d0d998 i18n(it): translate the dynamic labels the Italian catalog was missing (#3742)
Adds the 1,287 rows the other catalogs have and Italian lacked (filter and adjustment parameters and choices, Camera Raw, Lighting Effects, History and Layers labels, metadata, dialogs), fixes mistranslations in existing rows (Facet, Gradient stops, Save As Preset, Canvas, Spot Color) and uses one term per concept (Predefinito, Traccia, clic, scorciatoia, filmato).

Factors the Korean generated-label check into missing_generated_filter_labels and runs it for Italian too. Adds docs/localization-it.md and updates localization-parity.
2026-10-12 04:38:03 -04:00
FredApps 62f4b9d453 Menus: keep clear of the taskbar when Wayland hides the window position (#3737) (#3739)
* test(ui): scaffold reported menu viewport coverage (#3737)

* chore: opt FredApps into public profile credit

* Menus keep clear of the taskbar when the window position is unknown (Wayland) (#3737)

On Wayland winit can't report a window's position, so egui has no
inner_rect/outer_rect, only the monitor size. work_area::visible_part
then returned the whole window, and fit_window never maximized an
oversized window, so long menus (Layer, View, Window...) were sized to
the full window and ran under a bottom panel. Without a position, cut
the window to the monitor size less the taskbar (the most that can be
visible wherever the window is), and use the content size as the
outer size for fit_window.

Replaces the scaffold test with one that fails on main.

---------

Signed-off-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-10-12 04:33:54 -04:00
Giuseppe 986f977a01 Fix: video frames to layers working using folder dialog request (#3513) (#3718) 2026-10-12 04:33:48 -04:00
UnemarsandClaude Opus 5.5 986f68f969 Toolbar: the colour chips sit right under the tools again (#3745)
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>
2026-10-12 04:33:36 -04:00
403e321034 Select: open the Save Selection and Load Selection dialogs (#3738)
The menu items ran the engine commands with no params: Save Selection
silently saved into a new alpha channel and Load Selection failed with
"missing `channel`". The form dialogs already existed in view_cmds but
the two ids were missing from wraps(), so they were never shown. Load
Selection now only offers the layer mask when the active layer has one.

Fixes #3713

Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 04:33:25 -04:00
FredApps 4cf8a5fd05 Canvas: wheel and pinch zoom over the pasteboard keep the image in view (#3720) (#3731)
With the pointer on the gray pasteboard, wheel/Alt-wheel/pinch zoom anchored on the pointer,
so each notch slid the image away from it and out of the window. The anchor is now the
nearest point of the image (clamped in document space, so rotated and flipped views work);
over the image nothing changes.
2026-10-12 04:33:12 -04:00
MrCloudy2andClaude Opus 5.5 97c6769d6b Glows: Photoshop's precise ramp, and centre-source glows inverted after the contour (#3578)
Measured on Photoshop 27.11 (scripted: a 128 px square with one glow,
sampled pixel by pixel across its edges; outer and inner, both
techniques, sizes 10 and 20, spread 0/40 %, choke 0/30 %, Range 50 %):

- A Precise glow is 1 - D / (size + 1) beyond the solid spread/choke
  part, D being the centre-to-centre distance to the shape's edge.
  PhotoCraft's ramp ended at `size` and the default Range then doubled
  it, so it stayed solid for half the size and fell off steeply: up to
  122 levels off. The raw coverage is now 0.5 at the edge falling to 0
  at size + 1, so the shared Range step (v / range) gives Photoshop's
  ramp at the default 50 %; other Range values are unmeasured.
- A centre-source inner glow is exactly 1 - the edge glow, taken after
  the contour and Range. PhotoCraft inverted before them: the softer
  centre glow was 115 levels off, the precise one 64.

All 16 glow cases are now within 2 levels (10 were 58 to 122 off);
softer edge and outer glows are unchanged. On the GPU: the same ramp in
fs_mglow, and an "invert after the contour" mode (p0.y = 2) in
fs_mfinish for glows only (Satin keeps its own invert). The 44 GPU
parity tests pass. Corpus: the two Photoshop precise-glow oracles go
from 0.31 / 0.37 to 0.05 worst error (curved anti-aliased shapes,
still not under 2/255); nothing else changes.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 04:32:33 -04:00
MrCloudy2andClaude Opus 5.5 78c611007b PSD: alpha channel resources count the merged transparency, 13-byte DisplayInfo (#3577)
Photoshop 27.11 refused two of PhotoCraft's re-exported corpus files
(ag-psd layer-mask/src.psd, psd-tools effects/stroke-without-vector-mask)
with "Could not import the clipboard because an unexpected end-of-file
was encountered". Both carry transparency in the merged image plus an
alpha channel: PhotoCraft wrote two extra channels but named and
described only one in resources 1006, 1045 and 1077. In every corpus
file that has them (177, Photoshop's included), those resources hold one
entry per extra channel, the merged transparency first.

- Export: a "Transparency" entry leads the names and DisplayInfo when
  the merged image carries transparency and there are alpha channels.
- DisplayInfo (1077) records are 13 bytes, as in all 166 corpus files
  that have one; PhotoCraft wrote and read 14 (the older 1007 layout),
  which shifts every channel after the first.
- Import skips the transparency's entry, so an alpha channel keeps its
  own name and options ("Alpha 1", not "Transparency").

Corpus oracles and round trips are unchanged (io 148/170, psd-tools
237/309, Photoshop 137/258; every round trip renders the same).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 04:22:10 -04:00
dac995ee6c Edit › Transform › Skew: corners and sides move along one axis, like Photoshop (#3437) (#3590)
* Edit › Transform › Skew: corners and sides move along one axis, like Photoshop (#3437)

In Skew mode a corner drag scaled the box and the side handles moved
freely. Checked in Photoshop: a corner moves alone, horizontally or
vertically (whichever way the drag mostly goes); the top and bottom
handles slide only horizontally and the left and right ones only
vertically; Option moves the opposite corner or edge the other way. On
editable type a corner slides its whole edge instead, so the text stays a
parallelogram and remains live type. Free Transform is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Skew mode: a drag outside the box doesn't rotate it; ⇧ changes nothing (#3437)

Checked in Photoshop. A click outside still commits.

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>
2026-10-12 04:18:33 -04:00
agent-346022andphotocraft-agent UNCONFIGURED 8c6e4a3abc fix(engine): image.canvasSize rejects unlisted anchors instead of landing between two (#2946) (#3572)
anchor_factors resolved the x and y axes independently and with different matching
rules (x looked for a capitalised "Left"/"Right" or the exact word, y for a lowercase
"top"/"bottom" prefix). A misspelling matched one rule and missed the other, so
"topleft" anchored top-centre and "TopLeft" left-centre -- anchors the command does not
offer -- with no error.

Match the anchor exactly against the nine documented values via one ANCHORS table;
anything else is a BadParams error naming `anchor` and listing them, as #507 settled
for other command params. The nine valid values keep the exact factors they had, and an
omitted anchor still means center.

Test covers the nine offsets, the omitted default, and the misspellings from the issue
("topleft", "TopLeft", "TOPLEFT", "bottomleft", "bottomright", "middle", "", "NOPE").

Fixes #2946.

Co-authored-by: photocraft-agent UNCONFIGURED <unconfigured@invalid.local>
2026-10-12 04:13:20 -04:00
Nigel Ihza Firdausy 84aa8df05a fix(transform): keep nearest-neighbor previews on original texels (#3688) 2026-10-12 04:10:55 -04:00
David MannandClaude Opus 5.5 b623db1ed7 Crop tool: a resized frame snaps its handle, not the pointer (#3638) (#3640)
With Auto Center Preview off, or in Classic Mode, dragging a crop edge or
corner snapped the pointer, which grabs the handle up to the 8 px grab
radius away from it. The snap line showed on the target while the frame
edge stopped as far from it as the grab offset, so recropping toward the
pixels a Delete Cropped Pixels-off crop kept left a transparent strip
along the top and sides (or cut a few pixels off).

The crop handle snap (snap_ui::crop_handle) now covers every resize: it
snaps the handle of the frame crop_ui would make and moves the pointer by
the difference, 1:1 without Auto Center Preview and holding the ratio on
a corner as before.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 04:10:50 -04:00
agent-346022andphotocraft-agent UNCONFIGURED 55b3301384 test(ui): marquee shift+alt before the drag intersects the selection (#208) (#3624)
The marquee already picks the selection mode from the modifier held at the press
(shift adds, alt subtracts, shift+alt intersects) rather than constraining the shape.
marquee_tests.rs covered add and subtract but not intersect; this adds that case,
the one gap BUG-208-4 still names.

The test fixes an existing selection, holds shift+alt at the press (then releases so
the drag stays a plain rectangle, isolating the mode from the square/centre shape
constraints) and drags a second rectangle. It samples three points -- one in the
overlap, one only in the first rectangle, one only in the dragged rectangle -- so it
passes for intersect and fails for replace, add or subtract.

Test only; no production change.

Co-authored-by: photocraft-agent UNCONFIGURED <unconfigured@invalid.local>
2026-10-12 04:10:45 -04:00