A Move-tool click on the pasteboard with Auto-Select only cleared the layer
selection (#4991) when nothing was hit there, but `layers_hit` was not clipped
to the canvas: fill layers and pixels hanging past the edge were hit on the
pasteboard, so the click selected a layer that isn't drawn there instead.
Outside the canvas of a document without artboards, nothing is hit now, and
the layer marquee is clipped to the canvas the same way. Artboard documents
keep picking over the pasteboard, where they draw their layers (#5712).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Bound PSD import pixel storage before decoding
* Include ZIP inflater workspace in PSD preflight
* PSD import: budget unknown-mode layers, stop after a fatal layer error, keep best-effort for non-allocation limits (#8593 review)
- free_group's brush check builds the set of group names once instead of
scanning every preset for each numbered candidate.
- reimporting_a_gradient_file_asks_before_touching_its_group covers the
gradient branch of the prompt: no prompt for a new group, Cancel imports
nothing, Import as New Group adds "Fire (2)", Replace swaps "Fire" alone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Importing an .abr or .grd whose name matched an existing group silently
deleted that group's user presets (edited or moved-in ones included); the
preset libraries are outside document history, so it could not be undone.
- brush.presets.importAbr / gradient.presets.importGrd: `replace` now
defaults to false; a taken group name imports beside it as "Kit (2)",
"Kit (3)"…, and the result's `group` names the one it landed in.
- UI: when the group already holds the user's presets, a prompt asks
Import as New Group (default) / Replace (can't be undone) / Cancel.
Agents drive it with ui.dialog.set {"choice":"replace"} / confirm / cancel.
- The status bar names the group the file landed in.
Fixes#10640
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Artboards share one canvas, so a layer dragged from one board onto another in
the Layers panel (layer.moveTo, including the Alt-drag copy) changed parent but
kept its old canvas position, outside the board it now belonged to, and the
board clipped it away.
apply_move now shifts a layer that lands on a different board by the offset
between the two boards' origins, through move_layers (the Move tool's path).
Reordering within a board, leaving or joining a board from the top level,
position-locked layers and moving a board itself are unchanged. The Layers
panel's drop preview uses apply_move, so it shows the same result.
Locks are read before the drop as well as after, so a layer whose position
was locked only through the board it left stays put (review follow-up).
Co-authored-by: Claude <noreply@anthropic.com>
The author's main merge brings one new test (full coverage keeps Threshold over
extreme float channels) and rewords the #9483 tone comment; its main side
(b1cfff9d3) is already in the batch. Docs keep the batch's newer status lines and
revision rows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Channel targeting routes an allow-list of pixel-editing paint commands, not every
`paint.*` id: the Mixer Brush's Alt-click load and Symmetry no longer follow a colour
set or get refused with "Rasterize the layer" on a fill layer. Patch and Content-Aware
Move stay routed (#9274).
- Native vector save-back checks the preview SVG as strictly as stored previews are
checked on load and PSD export, so a preview with an unreadable embedded image is
refused instead of stored and later dropped (#8571).
- .pcraft blobs read their zstd frame with framing headroom, so an incompressible
preview right at its 16 MiB limit still loads; the limit still bounds the data.
- Levels/Curves recall (Alt+shortcut) decides the combined Lab record from the target
the dialog captured, as remembering does, so a viewed layer mask keeps its settings
beside a Lab colour set (#9274).
Each comes with a test that fails without it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reference application's Liquify has Twirl Clockwise only; holding
Alt (Option on macOS) while using it twirls counterclockwise. PhotoCraft
had an extra Twirl Counterclockwise tool on the strip, and #9598 had just
given it a mirrored icon.
Twirl Counterclockwise leaves the tool strip (LiquifyTool::ALL, 11 to
10 tools) and the rotate-ccw icon #9598 added goes with it (asset,
icon_data entry, ATTRIBUTION count back to 113). Alt already reversed
Twirl Clockwise at stroke-down; that stays. The TwirlCcw variant stays as
a stroke's direction: Alt-strokes record `twirlCcw`, and filter.liquify
strokes and older saved strokes naming it still replay as Twirl
Clockwise reversed (the command doc says so). Selecting it as the tool
(control channel `tool: "twirlCcw"`, or a preference saved by an older
version) is refused with an error that says to choose twirlCw and hold
Alt; the rest of the settings still apply and the dialog opens.
Tests: the_tool_strip_has_no_twirl_counterclockwise (no such tool, label
or missing icon on the strip, C is Twirl Clockwise, `twirlCcw` as the
tool is an Err and leaves the tool, an old saved preference naming it
opens); alt_with_twirl_clockwise_twirls_counterclockwise (the field
displacement a dab leaves right of the centre points up without Alt and
down, mirrored, with Alt). twirl_counterclockwise_has_its_own_mirrored_icon
and the twirlCcw row of alt_reverses_twirl_and_swaps_pucker_and_bloat
are removed. The i18n labels for Twirl Counterclockwise stay (the
variant's label).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#9594 fixed type copied between documents of different resolutions
re-laying out at the wrong size on its next edit by running the Image
Size resolution step on the copies: the type kept its point size
(24 pt) and got a compensating transform of src/dst ppi, with box and
path text scaled up to match.
The reference application, measured by script, keeps the pixels and
rewrites the point size instead: 24 pt copied from a 72 ppi document
into a 300 ppi one becomes 5.76 pt (24 x 72/300), and 24 pt from
300 ppi into 72 ppi becomes 100 pt. There is no transform, the
Character panel shows the rewritten size, and later edits keep it.
adapt_copies, which both Copy Layers to Document (Duplicate Layer to
another document, dragging layers between documents) and Paste of a
copied layer go through, now calls canvas_geom::retarget_type_resolution:
every size in points is multiplied by src/dst ppi (font size, leading,
baseline shift, first-line/start/end indents, space before/after) and
the PSD TySh is rebuilt. Box, path and transform are in pixels and stay
as they were; tracking, kerning and auto leading are relative to the
size and stay too. A resolution that isn't a positive number counts as
72 ppi (what the text engine lays it out at), and the scaled sizes stay
finite and above zero. Image Size keeps its own behaviour.
Tests: copy and Paste in Place, 72->300 (5.76 pt) and 300->72 (100 pt):
leading, baseline shift and indent scale, tracking stays, no transform,
the same ink bounds as the source, and after an edit the size is kept
and bounds and coverage match the source's render. Paragraph text keeps
its box and wrapping. Bad and absurd resolutions don't panic or produce
non-finite sizes. The #9594 test is replaced by these.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#5176 made ±100% move the edge by max(radius, feather, 2σ smooth), at
least 1 px. Measured in the reference application (a 100 px square,
Radius/Feather/Smooth each varied, Shift Edge -100/-50/+100):
- A hard selection does not move at all, at any Shift Edge.
- On a feathered edge the result is a fixed per-pixel curve, the same at
2, 5 and 10 px feather: the pixel's odds a/(1-a) are multiplied by
49^(shift/100). At -100% a 50% pixel becomes 2%, at -50% 1/8, at
+100% 98%. The fit holds within ±0.05 in log-odds on every sample.
So Shift Edge is now that curve, applied last as before. It needs no
neighbours, so the morphology callback and the halo/spread it added are
gone, with the ignored benchmark that timed it (matting::morph itself
stays for Select › Modify and the mask view).
Tests: a hard edge is unchanged at ±50/±100%; seven measured
before→after coverage pairs; a feathered edge is pulled in; the engine
refine tests now check the hard edge stays and a feathered one moves.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts resolved as unions of both sides: codecs re-exports (PngRows + TiffBands), smart_cmds source imports
(main's source_surface_from_buffer and cache API with #8571's svg_source_image_in/convert_svg_source; main's
duplicate rendering import dropped), io flat.rs test modules (#8536's banded TIFF tests and main's PNG ones),
menus/invoke.rs (#8571's smart SVG intercept and main's layer.selectUnderCursor), state.rs (#8311's
type_box_overflow and main's type_insertion_style), the canvas_geom test keeps the is_finite guard, docs
status lines keep both change lists, i18n catalogs keep both sides' rows (hi.tsv: main's wording where both
added the same string).
Integrate main b1cfff9d3, preserving both canvas and Curves test suites and progress-document updates. Match the fused channel-selection blend to main's HDR-safe formula and cover full-coverage edits over extreme float values.
Liquify, Adaptive Wide Angle and the Filter Gallery refused to open with a
layer mask, a smart object's filter mask, an alpha channel or the Quick Mask
targeted ("This filter workspace requires layer pixels or native colour
channels"). Without a colour set they now open on the layer or smart object
again and send no `target`, so the command routes the targeted surface as
before. With colour rows on a smart object they refuse at open, as the
command refuses at OK.
Colour rows stay selected across layer switches, so with Red still targeted
the Brush on a Levels adjustment layer was refused ("Rasterize the layer
before editing colour channels") instead of painting its mask, and type,
shape, smart object and fill layers skipped the rasterize prompt. A colour
set now targets raster layers only; several rows still stop Move and
transforms on any layer, at press as before.
Clicking a pixel layer's mask thumbnail (or adding a mask to it) with colour
rows selected left the rows targeted, so the Brush painted Red instead of the
mask. Targeting the mask now moves the Channels target onto it (a view
change, no recorded step); clicking a row again takes the rows back.
Edit > Stroke and the Magic and Background Erasers are now routed but still
edit the layer with an alpha channel targeted, so Edit > Fade faded the
untouched channel. Fade now picks the channel or Quick Mask only when the
command changed it.
Main's #9483 mixes selections as o*(1-k) + a*k; the fused colour-set tone
path still used o + (a-o)*k, so fused_tiles_match_scalar_oracle_* failed
after merging main (16-bit Levels with a selection). It now mixes the same way.
Co-authored-by: Vlad Grankovsky <4578120+hidoba@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
748 upstream commits: append-only conflicts (i18n, ui-parity, people.toml,
the tools module lists) keep both sides. Scorecard: 12 of 183 settings do
nothing; docs/ui-parity.md follows it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TYsCL5E2GM6hUabP9dmYAv
UPDATE_APP (native builds; craft-update is a native-only dependency) carries our 128 px icon and accent (#2f7bf5, its colour in ArtCraft Launcher). The updater, the self-update
helper and craft-update's startup prompt all get them through crate::UPDATE_APP; the
prompt draws the icon, the accent and "by ArtCraft" (craft-update's default developer).
craft-update is pinned to storytold/craft-libs main at 1065653e7 (#434).
Clippy fixes for Rust 1.99 in untouched code (same logic):
- apps/photocraft/src/app_dirs.rs
- crates/codecs/src/codecs/via_image.rs
- crates/engine/src/canvas_geom/tests.rs
- crates/ui-egui/src/panels/blend_preview.rs
217 upstream commits. Upstream added a colour swatch (Select and Mask's
overlay colour): it now opens the Color Picker titled "Color Picker
(Overlay Color)". Content-Aware Fill and Select and Mask leave their keys
to an open Color Picker, as they did for the swatch popup it replaced
(test keys_wait_while_the_overlay_colour_picker_is_open).
docs/ui-parity.md keeps the scorecard's 12 of 182.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TYsCL5E2GM6hUabP9dmYAv
* packaging: Flatpak finds and prints to CUPS printers
The Print dialog listed no printers in the Flatpak: the runtime has libcups but not
lpstat/lp, the sandbox couldn't reach the CUPS socket, and driverless network printers
(lpstat -e) need Avahi. Bundle lpstat/lp/lpoptions from CUPS 2.4.20 (Apache-2.0,
statically linked, licence installed) and grant --socket=cups and
--system-talk-name=org.freedesktop.Avahi in both manifests.
* print: lp takes the job only when CUPS lists a printer; the only printer by default; ~/.cups read-only
Review (CodeRabbit) on bundling lp in the Flatpak:
- With lp present but CUPS down or empty, the job failed on lp instead of reaching the
IPP route and the print portal. lp_route now gives up the route when lpstat lists no
printer, before anything is submitted.
- With no printer named, no default and one queue, IPP picked it but lp failed. lp_route
names the only printer.
- The sandbox ignored the user's default printer, saved options and client.conf server:
grant ~/.cups read-only in both manifests.
* print: no printers for lp means the portal, not IPP; bounded listing; CUPS socket wrapper
Review:
- When lp is there but CUPS lists no printer, the job went to the IPP route, whose
server detection ignores ~/.cups/client.conf and could print to a local default
printer instead of the configured server. It now goes to the print portal, or fails
with no portal.
- The lpstat listing before a job is bounded (10 s, then lp takes the job as before)
and runs outside SPOOLER, which guards only Windows driver calls.
- Flatpak mounts the host's CUPS socket at /var/run/cups/cups.sock whatever path
client.conf or CUPS_SERVER names; the CUPS tools now run through a wrapper that
points a socket path missing in the sandbox there, leaving server names alone.
* packaging: redirect a custom CUPS socket only when it is the one Flatpak mounted
Review (CodeRabbit): Flatpak picks the socket it mounts at /var/run/cups/cups.sock
with stricter rules than CUPS (flatpak-run-cups.c: a socket CUPS_SERVER, else the
first line spelled exactly "ServerName /path" in ~/.cups/client.conf, else the
default), while CUPS reads the keyword in any case and takes the last ServerName. The
wrapper now redirects only when CUPS' socket is the one Flatpak chose, so a job can't
reach a different server; otherwise CUPS fails on the missing path.
* print: a printer listing past its deadline kills and reaps lpstat
Review: the 10 s limit stopped waiting but left a hung lpstat and its thread
running, so repeated print attempts could pile them up. photocraft now runs the two
lpstat listings itself (craft-print's lpstat_command and an equivalent lpstat -e,
parsed by printers_parsed) and kills and reaps each one at the shared deadline. The
output is read on a thread that ends with the process, so nothing outlives the job.
* packaging: never redirect a CUPS_SERVER the sandbox was given
Review (CodeRabbit): Flatpak picks the socket it mounts from the host's CUPS_SERVER,
so one set by flatpak run --env or an override may not be what it mounted. The
wrapper now leaves any CUPS_SERVER alone and redirects only a client.conf socket that
matches Flatpak's choice; otherwise CUPS fails on the missing path instead of
reaching a different server.
* packaging: point CUPS at the mounted socket only when it is the one Flatpak chose, else fail
Review (CodeRabbit): with a custom socket listed first and the default last in
~/.cups/client.conf, Flatpak mounts the custom one at /var/run/cups/cups.sock while
CUPS picks the last line, finds that path, and talks to the custom server. The
wrapper now compares CUPS' socket with Flatpak's choice even when the path exists:
equal, CUPS is pointed at the mount; different, the tool fails with an error instead
of reaching another server.
Also (CodeRabbit):
- A printer named in the job goes to lp even when CUPS lists none: listing can be
forbidden while printing is not, and the portal can't honour a name.
- A failed lpstat -e (CUPS unreachable, listing forbidden) means the list can't be
told, so lp takes the job, instead of an empty list sending it to the portal.
- A listing longer than 1 MiB counts as no answer, instead of growing without bound.
- The kill test checks the process with kill -0, which works on every Unix.
- docs/printing.md: the only-printer fallback is on the lp and IPP routes, and the
portal is Linux only.
Conflicts with main's module moves resolved: driver_printable takes main's
spooler-held settings_for_layout (which already wraps the page query in
with_spooler) and drops the now unused settings/pg lines; take_turned_page and
its test move to dialogs/file_ui/print.rs and dialogs/file_ui/tests.rs;
crates/engine/src/cmd/print_cmds/tests.rs keeps main's version (the PR did
not change it in its own commit).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main's noop_step now takes the step's params (an Arrange that cannot move the layer is a no-op too); it is public for the CLI's run and batch, which pass them.
- Smart-object contents (Replace Contents, Convert to Layers, Stack Mode,
internal Edit Contents) refused any SVG whose canvas area times its
node count passed 24 MP, e.g. a 1920x1080 layout with 12 shapes.
decode_source now uses the bounded image parse with File > Open's
layered import (import_svg_bounded_images). Convert to Layers' shape
cache cap is 1 GiB in the document's format, the import's own budget,
not 24 MP (four full-canvas shapes in a 4K document failed).
- The bounded resolver failed the whole drawing on an embedded image it
can't read (an unknown MIME type, a corrupt PNG); it now leaves it out
as usvg does, so such SVGs place and render again. Byte and decode
overruns stay errors.
- resvg gives each embedded SVG image a scratch pixmap the size of its
target and frees it after, so the scratch budget counts the deepest
chain of nested SVG images, not every sibling: 12 SVG icons on a 2 MP
canvas, or one at a 12 MP render, no longer fail.
- Main's shared nesting budget (#9397) now covers the bounded path too:
511 + 511 levels across a drawing and its embedded SVG overflowed a
1 MB stack.
- An AI working copy is regenerated through VectorCraft only when
VectorCraft is the editor; Illustrator or another program gets the
original bytes. VectorCraft is recognised by bundle id, a known
install, or its name (vectorcraft[.exe], vectorcraft-<version> AppImage,
ai.storyteller.vectorcraft).
- updateSvg refuses an AI/PDF/EPS source (updateVectorSource saves it
back in its own format); the doc no longer calls updateSvg "existing".
- Edit Contents on contents already open in the internal editor switches
to that document, as main does, instead of an error.
- Stack Mode on an SVG refuses up front when one float frame per layer
would pass 1 GiB (the layered import no longer refuses for it).
- A stored display preview still fails on an embedded image it can't
read (strict budget, its own import_svg_preview_with_work; external-
editor saves and conversions leave the image out as placement does); skipped embedded SVGs still use up the XML byte
budget; an image under an unknown MIME type or with no pixels is left
out uncharged.
- Merge: the PSD import reads a preview's native source from the
document's global blocks; main's owned open moves them out of the
file, so every stored preview was dropped.
- Main's strict Clippy rules on the PR's app code (explicit import, a
Unix-only test helper, one long opt-in test).
Co-authored-by: Mocky Digital <282319807+mockydevs@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts: svg.rs (main's bufread inflate and shared render helper, kept
with the PR's bounded raster path), psd_import.rs (the PR's vector
preview moved into main's smart_content), menus/invoke.rs (the SVG
intercept moved into main's split invoke_unguarded), rendering.rs and
the ui-egui import lists (main's explicit imports), Cargo.lock, docs
status lines (unions).