* Color Picker: sample colours from the image with a pipette
With the Color Picker open, the pointer over the image turns into a
pipette, as in Photoshop. A click or drag there samples the composite
colour into the picker's new colour, and OK applies it. This works
whatever tool is selected; Space and middle-drag still pan. Over the
dialog the pointer is the normal arrow.
Agents get the same: `ui.pointer` (MCP `ui_pointer`) samples into an
open picker instead of driving the tool.
* Color Picker tests: rename the eyedropper test's colour helper after the rebase
#485 added a `color` test helper in the same module.
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>
* Layer Style: the shadow/bevel Angle slider now moves the light (#350)
The Drop Shadow, Inner Shadow and Bevel & Emboss dialogs exposed an Angle
slider but no "Use Global Light" checkbox. The engine defaults
use_global_light to true, so the compositor used the document's fixed
global light angle and ignored the per-effect angle — dragging Angle did
nothing.
Add Photoshop's "Use Global Light" checkbox to those three effects,
round-trip useGlobalLight through the dialog fields, show the effective
(global light) angle when it is on, and — when it is on — drive the
document's shared light angle from the slider so the shadow follows it,
exactly as in Photoshop.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
* fix(ui): preserve edited layer-style global light
Apply a layer-style global-light angle only when it differs from the dialog's initial angle, so later unchanged effects cannot override a Bevel & Emboss edit. Use the current light angle for new global-light effects and add translations for Use Global Light.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e5e4c3e6-e406-4010-b05d-5042f9f4e4fc
---------
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
Copilot-Session: e5e4c3e6-e406-4010-b05d-5042f9f4e4fc
The buttons show their key in parentheses and the D, C and S keys answer
the prompt (Revert prompt: (C)ancel, (R)evert). Translated labels that
don't start with the key letter get it appended ("Guardar (S)"), so the
catalogs are unchanged.
Tab / Shift+Tab move right / left through the buttons and wrap. egui's
own Tab order follows the right-to-left layout, so Tab used to move left.
Buttons from widgets::primary_button / secondary_button now draw an
accent ring while they have keyboard focus.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Type layers keep their name in step with their text (#483)
A type layer is named after its first line when it's created, but
only the Type tool's first editing session renamed it again. Editing
the text later (clicking back into the layer, type.edit, Paste Lorem
Ipsum into it) left the old name, so a layer re-typed from the
placeholder kept saying "Lorem Ipsum".
Like Photoshop, a type layer whose name is still the one its text
gives it now follows every text edit, in the same history step. A
layer named anything else (given a name at creation or renamed later)
keeps it. Paste Lorem Ipsum no longer gives its new layer a fixed
"Lorem ipsum" name, so that layer follows its text too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Type layer names: the first non-empty line, the same in the Type tool and the engine
Review on #501: the Type tool named a new layer after its first
non-empty line, the engine after its first line, so text starting with
a blank line stopped the name following the text. layer_name now takes
the first non-empty line and commit() uses it. Also drop a product name
from a doc comment.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Add rendering policy with legacy preference migration and safe adapter selection
* Add rendering mode preferences and GPU fallback recovery warning
* Complete CPU fallback recovery and validate rendering mode UI
* Simplify GPU recovery and reuse preference persistence
* Rendering modes: rebase onto the preferences merge/retry save path; drop docs screenshots
save_preferences now writes through write_prefs (merging other instances' changes) and
only marks the revision saved when the write succeeds, so a failed recovery choice is
still retried. Removes a leftover diff3 marker in es.tsv, the docs/images/rendering-*.png
screenshots and their ATTRIBUTION row (no binary assets in the repo), and regenerates
docs/scorecard.md for the new performance.renderingMode preference.
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>
The Color Picker's hex field was rebuilt from the colour every frame,
so anything short of six valid digits was thrown away and it stayed at
000000. It now shows what is being typed while it has focus.
The Lab and CMYK fields applied every keystroke through clamped sRGB,
so typing "60" into L went through L = 6 first and changed a and b.
The dialog now keeps the typed Lab or CMYK values while they still
produce the colour, as it already did for HSB.
The Color panel's Hue and Saturation fields (Studio and Classic
themes) reset to 0 on black and greys; they now keep the typed values.
parse_hex no longer panics on multi-byte text.
* Linux: AppImage update information and a .zsync beside it (#349)
appimagetool now embeds
gh-releases-zsync|<owner>|<repo>|latest|photocraft-*-linux-<arch>.AppImage.zsync
(owner/repo from GITHUB_REPOSITORY, default storytold/photocraft), so
AppImageUpdate, AppImageLauncher and the like can find the next release and
fetch only the blocks that changed. With zsyncmake on the host it also writes
the .zsync, into its working directory, so the script runs it in $DIST and
warns when the file is missing.
The release job installs zsync and checks the embedded string and the .zsync
before uploading; docs/releasing.md lists the new asset.
Verified locally with packaging/linux/package.sh --skip-build --formats
appimage and zsyncmake from Ubuntu 22.04: --appimage-updateinformation prints
the string, and the .zsync's URL is the AppImage's basename.
* package.sh: absolute AppDir and appimagetool paths before running in $DIST
appimagetool now runs inside $DIST so zsyncmake writes the .zsync there,
which broke relative CARGO_TARGET_DIR / APPIMAGETOOL paths (AGENTS.md
suggests target/agent-<name>). Resolve both first, remove a stale .zsync
so a missing zsyncmake is reported, and list the .zsync and zsyncmake in
the header.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Decoders now report what the decoded image doesn't show in a new
`Image::warnings` (`photocraft_codecs::DecodeWarning`), and `image_to_document`
turns them into import warnings, so they reach `photocraft-cli info` /
`convert` stderr, `doc.open` over MCP and serve, and the app's open notice
through the existing import-warnings path.
* #523: APNG (acTL frames, plus a default image outside the animation),
animated GIF (image descriptors counted by walking the blocks), animated
WebP and multi-page TIFF (IFDs walked without decoding; reduced-resolution
copies and transparency masks are not pages) say "only the first of N
frames/pages was imported". Single images stay quiet; the kept frame's
pixels are unchanged.
* #518: a JPEG whose data runs out before its end-of-image marker (marker
segments skipped by length, scan data by byte stuffing) still decodes
leniently but warns "JPEG data ends early (the file is truncated or
damaged); part of the image is missing". A JPEG that ends before any scan
data is an error.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The VM's software Vulkan driver crashes (SIGSEGV) when several tests set up a GPU
device at once: about half of recent FreeBSD runs died within 0.05 s of a GPU test
binary starting 2-3 tests in parallel (drag_preview_canvas, color_managed_canvas,
adjust_preview_gpu), before any of them finished. Run photocraft-ui-egui's
integration tests with --test-threads=1 there; everything else is unchanged.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Preferences: saves merge with what another tab or window stored, and failed writes are retried (#475, #476)
A save now writes only the values this instance changed since it last
loaded or saved them over what storage holds (a three-way merge over the
JSON tree; arrays are leaves), so a second browser tab no longer reverts
the other's changes. The saved revision moves only when the write
succeeds: a failure is retried after 2 s, doubling to 30 s (sooner after a
newer change), with a status message on the first failure and a notice
while it keeps failing. Shared by the desktop and web builds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Web: a startup that never gets going shows what may have failed and a Reload link (#477)
When the .wasm fails to download or doesn't match the page, no Rust runs,
so the loading page itself (CSS only, no script) adds the hint after 20 s.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* PSD: reject a header with zero width or height (#519)
A 0xN header opened as an empty document and saved back as another
zero-sized PSD or .pcraft. Header::validate now requires at least 1x1
(the spec range starts at 1), and PSD export validates the header so a
zero-sized canvas is never written.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* .pcraft: documents with deeply nested groups load again (#517)
serde_json's 128-level recursion limit made bundles with 40 or more
nested groups unreadable after a successful save. The manifest is now
parsed with that limit disabled behind a linear JSON depth check, layers
are decoded and converted one level at a time with small stack frames,
and groups may nest up to MAX_GROUP_DEPTH (100) levels, which load and
save within a few hundred KiB of stack. Saving a deeper document fails
instead of writing a bundle that can't be opened.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Export: convert non-sRGB RGB to sRGB for formats without a profile (#522)
GIF, BMP, QOI, TGA, ICO and Netpbm can't embed ICC, so a linear-light
or Display P3 document was written with values that only meant something
under the dropped profile (a linear EXR came out dark). export_flat now
converts such RGB to sRGB through photocraft-cms (perceptual, as Quick
Export does) for every photocraft_io::export route. The EXR/HDR linear
conversion shares the same banded helper.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Export: composite transparency over white for formats without alpha (#521)
The CLI, MCP, Save As, Layers to Files and Layer > Export As dropped the
alpha channel when writing JPEG (or Radiance HDR), exposing the colours
stored under transparent pixels, while Export As and Quick Export
composite over white. export_flat now flattens over white too (no ink
for CMYK) and says so in its warnings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Command params that are not a JSON object are an error (#506)
`Session::dispatch` now rejects array, string, number and boolean params
with "invalid parameters for `<id>`: params must be a JSON object" instead
of running the command with every default; `null` still means no params.
The one check covers the control channel, `serve`, MCP (headless and
bridge) and the CLI, and batch steps fail per step. `photocraft-cli run
--new` prefixes its error with `--new`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Headless requests apply finished background jobs first (#503)
A `wait: false` job was applied to its document only by a later command
or `jobs.list`, so `doc.save`, export, `doc.inspect`, `doc.render` and
`session.list` (and their MCP tools) still read and wrote the document
without it. `Headless::sync_jobs` now runs at the top of every JSON-lines
request and before every headless MCP tool call.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* `serve` stdio keeps the session after a rejected request line (#505)
An over-long request line (or one that is not UTF-8) ended the stdio
session, losing every open document. It now gets one error reply, the
rest of that line is skipped without being dispatched, and the next
request is served. `read_bounded_line` reads bytes, so a cut inside a
multi-byte character is reported as too long instead of an I/O error.
TCP connections still close after an oversized line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* MCP `command_batch` keeps its step results when the reply budget stops it (#504)
The batch budget charged each step's plain JSON size, but MCP sends the
batch reply as escaped text content, so a batch the budget had already
stopped could still exceed the tool-result ceiling and come back as a
bare "response exceeds" error. MCP (headless and bridge) now uses
`BatchReplyBudget::escaped`, which charges the escaped size, through the
new `Headless::batch_with_budget`; `serve` keeps the plain budget.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: the 1 MiB request-line limit does not apply to MCP stdio (#529)
The book stated the 1 MiB request-line limit without naming a transport.
It applies to desktop and headless TCP and to the headless JSON-lines
stdio server; MCP over stdio reads its own lines and only its tool
results are size-checked. Say so in the automation security pages and in
the control protocol's transport limits.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* ui.inspect descriptions point to ui.menu.list for the menu tree (#510)
The control-protocol doc, the control.rs method list and the MCP
`ui_inspect` description said `ui.inspect` returns the menu tree; it
never did. They now say the menu tree comes from `ui.menu.list` (over
MCP, through `control_call`).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ui.menu.invoke and ui.dialog.confirm wait for the job they start (#511)
Since filters became background jobs, invoking a no-dialog filter from
the menu or confirming a filter dialog over the control channel replied
`{job, pending: true}` before the filter was applied, unlike v0.2.0 and
unlike `engine.execute`. All three now share one helper that runs the
command with script events off and, when it started a job, defers the
reply until the job's result lands; `"wait": false` keeps the immediate
`{job, pending}` reply. The now-unused `run_automation` is gone.
The new test drives Blur More through `ui.menu.invoke` and Gaussian Blur
through its dialog's OK, with and without `wait` (it failed before).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ui.pointer right-click opens its menu at the pressed point (#512)
A `ui.pointer` right-click listed the layers at the requested document
point but drew the layer menu at the canvas centre, and the Brush Preset
picker opened there too. Both now open at the point's screen position,
where a mouse right-click on it opens them. `ViewXform::active` gives the
active canvas's document-to-screen mapping as last laid out (the
construction several tests repeat); the centre stays the fallback when
the point can't be mapped.
The ui.pointer doc now lists `button`, both menus, and that top-level
modifier flags are ignored when `modifiers` is given.
The new test checks two points against the mouse path and the picker
position (it failed before).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* MCP ui_pointer forwards button and rejects unknown arguments (#514)
Bridge clients couldn't right-click through `ui_pointer`: it forwarded
only `events` and `modifiers` and silently dropped anything else
(`button`, a typo, a top-level `space`). It now forwards an optional
`button`, and `PointerParams` denies unknown fields, so the schema says
`additionalProperties: false` and a stray argument is an error.
The new bridge test checks the forwarded params, the rejection of
`buton` and top-level `space`, and the tool schema (it failed before).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Shortcuts pressed in one frame run in the order they were pressed (#440)
Undo then Save in the same frame dropped the Undo: only the first binding in table order ran. Each key press now dispatches in event order, and a shortcut that opens a dialog, prompt, menu or inline type leaves the frame's later presses to it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Shift+Delete opens Edit › Fill on Windows instead of cutting (#530)
egui-winit reports Shift+Delete as a Cut on Windows. Outside text fields, a Cut with Shift and without Ctrl is now the Shift+Delete press again, so it fills like Shift+Backspace; Ctrl+X still cuts and text fields keep their Cut.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Shape and text colour pickers stay put while dragging (#534)
The picker's width follows its RGB readouts, and egui placed the popup anew every frame: near the right edge of the window it flipped between opening right and left of the swatch, under the pointer. A swatch's picker now keeps the left edge it opened with, shifted left of the swatch when the window edge needs it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Elliptical Marquee selections record "Elliptical Marquee" in History (#513)
Both marquee tools commit through `select.rect`, which named every step
"Rectangular Marquee" whatever its `ellipse` param. The step is now named
after the tool that made it; the rectangular marquee keeps its name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Preset filters record their own names in History (#528)
Blur, Blur More, Sharpen, Sharpen More, Sharpen Edges and Despeckle run
a generic algorithm with fixed parameters and recorded its name
("Gaussian Blur", "Unsharp Mask", "Surface Blur"). The presets now live
in one table with their names, used for the history step and the job
progress text; Gaussian Blur, Unsharp Mask and Surface Blur keep theirs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Vertical type command no longer claims it renders horizontally (#446)
Vertical type has rendered in columns since #199, but the params text of
`type.orientation.vertical` still said it was "rendered horizontally for
now". It now documents the same `{"layer":id?}` as Horizontal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Frame from Layers is one history step (#496)
It grouped the layers through `layer.new.groupFromLayers` (its own
"Group Layers" step) and then recorded "Frame from Layers", so one undo
left the group behind. The grouping step is now folded into the frame
step with `purge_last`, as Transform Again on a copy does; if the group
can't be framed, the grouping is undone with nothing left to redo.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Undo and redo restore the layers each history state targeted (#495)
History states held only the document, so after undo or redo the active
layer stayed whatever it was: redoing New Layer left the old layer
active (a Fill then painted it), and undoing Delete Layer left the
restored layer unselected. Each state now also keeps the active and
selected layers it had when it was created (when the document was
opened, or right after the step that made it), and undo and redo target
them again where they still exist. Selecting a layer is still not a
step and doesn't change any state's target, so selecting another layer
and then undoing or redoing goes back to that state's target, as the
reference app's Step Backward does.
The History panel lists redo labels from a new `redo_labels` accessor
instead of cloning and replaying the history every frame.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Launch no longer deletes recovery entries right after loading them. A
recovered document adopts the entry it came from: its next autosave
replaces it in place (atomically) and saving or closing the document
removes it, so a second crash before the next autosave loses nothing.
New documents autosave under per-launch keys, so a document id reused
from an earlier launch never overwrites a surviving entry, and sidecars
whose key doesn't match their file name are ignored.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Gradients honour explicit transparency stops with colors, a preset or the current gradient (#465)
`paint.gradient` and `gradient.fill.create` dropped `transparency` unless `stops` was
given. Explicit transparency stops now replace the gradient's own opacity stops whatever
supplies the colours. One shared parser (`transparency_param`) serves paint.gradient,
gradient.fill.create/set and the presets; malformed stops are an error, not dropped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Smart filter commands check an explicit layer target, not the active layer (#466)
`layer.smartFilter.*` with `"layer":id` were refused when the active layer was not a
smart object with filters, although they edit the named layer. Their precondition is now
checked on that layer; the active layer stays unchanged, and without `layer` (menus)
nothing changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Gradients: reword the opacity-merge doc comment
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* CLI --quality refuses every value outside 1-100 (#491)
--quality was parsed as a u8 and clamped, so 0 and 101-255 were silently
accepted while 256 and above were rejected. Every value outside 1-100 is now
a bad --quality error, checked before any file is read or written.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* CLI batch: --format with a leading dot names outputs <stem>.<ext> (#490)
batch built output names from the raw --format value, so --format .jpg wrote
a..jpg. One leading dot is now dropped, as the save path and
file.automate.batch already do.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* CLI batch --actions takes recorded-action and droplet steps (#489)
batch read only {"command", "params"} objects, so the [id, params] pairs a
recorded action or droplet stores failed with 'missing "command"'. It now
uses the engine's action parser (automate_cmds::parse_action, split out of
parse_script), which takes [id, params], objects and bare ids, bare or
wrapped in {"steps": ...} or a droplet; {"actions": ...} still works.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* CLI batch refuses an --out folder that is the --in folder (#492)
Results are saved as <out>/<stem>.<ext>, so --out naming the input folder
silently replaced the originals. batch now compares the canonical folders
(relative spellings, trailing separators, symbolic links) and fails before
writing anything, naming the folder; the new --in-place flag opts in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- Roadmap (updated 2026-10-07): the M11 row notes the stricter CLI,
batch collision reports, per-step `wait` in MCP batches and safe
headless saves; the UI / UX row lists the keyboard-only shortcuts
(layer navigation, number keys, hardness, step and repeat), Alt-click
sampling, double-click for Layer Style, New from Clipboard, the
centred window and remembered Liquify settings.
- Scorecard: UI-217-1 is partial (Fill and Liquify settings persist);
UI-217-2's note points at the new `native_options()`. docs/scorecard.md
regenerated; docs/parity.md is unchanged (no new catalogue items).
- README: `<subcommand> --help` in the CLI snippet.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
On Linux, Ctrl+Insert / Shift+Insert become Copy / Paste on the canvas (the pixel
commands' Ctrl+C / Ctrl+V) and in text fields (Event::Copy; Shift+Insert asks
egui-winit for the clipboard text with ViewportCommand::RequestPaste).
On Windows egui-winit already maps both keys, but drops Shift+Insert when the
clipboard holds only an image. Its key release now pastes, like the existing
Ctrl+V fallback.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* 32-bit documents: Exposure works on the stored linear values
Their samples are linear already, so going through the 2.2 curve applied
the gamma twice. rgb32 and gray32 exposure.psd now match Photoshop within
0.5/255 (was 69/255 and 44/255).
* Raise the Photoshop oracle floor to 130
Estimate encoded raster and retained-data size with checked arithmetic, and select PSB when the PSD limit is exceeded. Keep forced PSB choices and document the completed scorecard slice.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f77709e9-6475-4953-ab57-b78347b4499d
Dialogs were anchored at the screen centre, so a body that grew (Layer
Style switching effects) re-centred the dialog and moved its title bar.
They now open centred, then pin their top-left after the first laid-out
frame: they grow down and right, and title-bar drags move the pinned
corner (still clamped on screen).
The Color Range resize test now asserts the top-left stays put (it
failed before the fix).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The live Move preview redrew only the moving layers' bounds clipped to
the canvas at drag start, then translated. For a layer larger than the
canvas, the strip sliding in from beyond the edge was never
recomposited, so the edge pixels smeared along the drag path. Keep the
bounds unclipped and clip them to the canvas after translating.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Show PhotoCraft star growth at the bottom of the README with a linked Star History chart using the date view and top-left legend.
Co-authored-by: Contributor <contributor@users.noreply.github.com>
The other hidden "quick transform" shortcut. Cmd+Alt+T (no menu item,
in Keyboard Shortcuts as "Free Transform a Copy") duplicates the active
layer, or with a selection copies the selected pixels to a new layer
(Layer via Copy), and opens Free Transform on the copy. OK leaves the
original in place and the transformed copy as one history step (the
duplicate's state is folded away); an untouched OK keeps the copy where
it is; Esc, or a transform that fails, undoes the copy and leaves
nothing to redo. `TransformSession::copy` marks such a session.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Replace the stale DrawCraft name in the contributor sibling list.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f77709e9-6475-4953-ab57-b78347b4499d
The frame drawn around paragraph text while editing had eight handles,
but a press on them only placed the caret. They now resize the box:
corners move two edges, side handles one, with a 4 px minimum. The drag
goes through type.edit with the session's coalesce key, so it is one
history step.
Opening any file Photoshop tagged with sRGB IEC61966-2.1 asked
"Embedded Profile Mismatch: sRGB IEC61966-2.1 vs sRGB IEC61966-2.1":
open_policy compared a hash of the ICC bytes, and Photoshop's v2 profile
(1024-entry tables) and the built-in v4 sRGB (parametric curves) are the
same colours in different bytes. 32-bit documents in Photoshop's linear
sRGB missed the stay-linear exception for the same reason.
Add Profile::same_colors: same colour space and every sample of a device
grid within dE76 0.5 through both (re-encoding costs 0.025; moving the
red primary 2% costs 1.0). Colours are compared rather than round-tripped
because CMYK -> PCS -> CMYK is not an identity even for one profile.
Of the 75 profile-tagged PSDs in the test corpora, all 75 prompted on
open; now 6 do (Adobe RGB, Gray Gamma 2.2, SWOP), each a real mismatch.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Include apps/photocraft in workspace default-members so a plain cargo build also builds the native UI.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f77709e9-6475-4953-ab57-b78347b4499d
To enlarge, carve_width_with finds seams on a shrinking copy and duplicates them, but it stopped once the copy was 1 px wide, so a 1 px wide image found no seam and came back at its old width. The caller then wrote a short buffer into the new size (the "region data length mismatch" assertion in write_region) or, with a height change too, transpose indexed past the end. The session's guard turned both into "failed with an internal error". The single column is now taken as the seam, so it is repeated; wider images are unaffected, because a round takes at most half the columns.
`prefs.get` and `prefs.set` route `colorSettings.*` to the colour state,
but `prefs.reset` only special-cased the whole section. A nested path
fell through to `Preferences::reset`, which has no colorSettings, so it
failed with "unknown preference". Take the default from
`ColorSettings::default()` and set it the way `prefs.set` does.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`dim_field` allows negatives for Canvas Size's relative mode, so a typed
0 width stayed 0 in the Image Size dialog, which previewed "0 px × 1 px"
while `image.imageSize` resized to 1×1. Clamp edited dimensions to one
pixel so the preview shows the size that will be applied.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
filter.blur.gaussian isn't registered, so the documented CLI run and the
agent loop failed with "unknown command". Pin the schema examples with a test.
* Number keys set opacity, Shift+[ / Shift+] step hardness, Shift+Cmd+A opens Camera Raw (part of #352)
More of the shortcuts users expect without a menu item:
- 1–9 set 10–90% opacity and 0 sets 100%; two digits typed quickly
give an exact value (4 5 = 45%, 0 0 = 0%). With the Brush, Pencil or
Eraser that's the brush's Opacity, and Shift+digit its Flow (Brush,
Eraser); with the Gradient or Paint Bucket their Opacity; with tools
that have no opacity of their own (Move, selection, Type…) the active
layer's Opacity, Shift for Fill. The locked Background, and tools
whose number is a strength or exposure, are left alone. Digits are
read by physical key, so Shift+digit works on any layout.
- Shift+[ / Shift+] step a painting tool's hardness by 25%. They're
checked before [ and ], whose egui key test ignores Shift.
- Filter › Camera Raw Filter… gets its default Shift+Cmd+A.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* opacity_keys: module doc without a product name
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The hidden shortcut for step-and-repeat was missing. A new command,
`edit.transform.againCopy` (no menu item, Cmd+Alt+Shift+T), duplicates
the active layer and repeats the last transform on the copy, as one
history step labelled "Transform Again", so repeated presses lay out a
row (or a ring) of copies one transform apart. The transform is worked
out before anything changes (`transform_again_params`, now shared with
Edit › Transform › Again), so nothing to repeat leaves no copy; a copy
that can't be transformed (position locked) is taken back with nothing
left to redo (`History::clear_redo`).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The layer shortcuts with no menu item were missing. A new module,
`layer_nav_cmds`, adds six commands that walk the Layers panel's rows
(a group's row before its children, collapsed groups as one row):
Select Layer Above / Below (Alt+] / Alt+[), Select Top / Bottom Layer
(Alt+. / Alt+,), and Add Layer Above / Below to Selection
(Alt+Shift+] / Alt+Shift+[). They stop at the first and last rows,
never dirty the document, and are listed under Layer in Keyboard
Shortcuts so they can be rebound. Agents run them like any command.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Windows places a new window that has no position by cascading it from
the top-left corner, so PhotoCraft opened at a different offset from
the corner on every launch. The window now starts centred on the main
monitor (eframe's `centered`), at the same 1440 × 900, which eframe
shrinks to the monitor first and `work_area::fit_window` still
maximizes when it doesn't fit (#315). Wayland compositors keep placing
windows themselves. The window options moved into `native_options()` so
a test can check them.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Every time Liquify opened, the brush went back to the defaults (Density
50 and so on). Its settings (tool, size, density, pressure, rate, mesh
and backdrop options) are now kept in `prefs.dialogs["filter.liquify"]`
whenever the dialog closes (OK, Cancel, Esc or the control channel) and
restored the next time it opens, also after a restart. The first use
still starts from the defaults with a brush about a tenth of the image.
`LiquifyOpts::apply` and `to_json` read and write the settings; the
control channel's setter now uses `apply` too, and a damaged saved value
is clamped or skipped, never a failure to open. The three cancel paths
share one `cancel`.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Any `--flag` a subcommand didn't know was recorded and ignored, so
`batch --fromat jpg` wrote the source formats and exited 0, `run
--param '{…}'` turned the JSON into a stray file name, and
`<subcommand> --help` ran the subcommand (`mcp --help` started a
server). One table, `SUBCOMMANDS`, now lists each subcommand's value
flags, bare flags and handler, replacing the shared VALUE_FLAGS list:
an unknown flag in either form (`--flag value` or `--flag=value`) is a
usage error naming it (exit 2) before any work, a value on a bare flag
is an error, and `--help` or `-h` after a subcommand prints usage and
exits 0. Every documented flag keeps working in both forms.
The usage text now lists `--quality` for `run` and `batch`, which
already honoured it, and the book's CLI page describes the stricter
parsing.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
File › New from Clipboard (`file.newFromClipboard`, right after New…)
opens the clipboard image as a document of its own: sized to the image,
holding it as one layer, in the pixel format it was copied in. It has
no default shortcut; one can be assigned in Keyboard Shortcuts. Edit ›
Paste (Ctrl+V) with no document open does the same, so a screenshot is
one keystroke from the start screen. Both read the OS clipboard only
when invoked, stay enabled from the clipboard service like Paste, and
are a quiet no-op when it holds no image.
Agents reach it through MCP and `serve` too: the command only uses the
session clipboard, so it joins the automation allowlist of file
commands. The menu label is in every catalog.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
`file.automate.batch`, `file.scripts.imageProcessor`,
`file.automate.lensCorrection`, droplets (which run batch) and
`photocraft-cli batch` named every output `<stem>.<ext>` in one folder.
When two inputs mapped to the same name (`a.png` and `a.jpg` saved as
JPEG, or the same file name in two input folders), the later save
replaced the earlier result while both were listed as written and the
run exited 0.
`engine::file_cmds::OutputClaims` remembers the outputs written in a
run, ignoring case as macOS and Windows file systems do. An input whose
output was already written is skipped before any work and reported in
`errors` (the CLI prints FAIL and exits non-zero); the first result is
kept. Runs whose names don't collide, including the same stem kept in
its own formats, are unchanged. The commands' params docs say so.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A double-click anywhere on a Layers row started a rename. Now only a
double-click on the name renames; the rest of the row follows the
layer's kind: an adjustment or fill thumbnail opens its Properties
(Layer Content Options), a Smart Object thumbnail opens its contents
(Edit Contents), and anywhere else opens Layer Style (Blending Options).
The Background still becomes a normal layer, mask thumbnails keep their
own clicks, and a command that's disabled for the layer does nothing.
docs/ui-design.md lists the row's double-click targets.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Holding Alt (Option) with a painting tool now works as the Eyedropper
for that press: a click sets the foreground colour and a drag keeps
sampling, without painting or recording a history step. The choice is
made when the press starts, so Alt pressed or let go mid-stroke never
switches between painting and sampling, and Control+Alt stays the
brush-resize drag. While Alt is held the cursor shows the Eyedropper's
crosshair. Agents get the same through `ui.pointer` with `alt`.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
MCP `doc_save {}` and `serve` `doc.save {}` fell back to the document's
own path for any format, so an agent that saved its work replaced the
input PNG or JPEG with a flattened, re-encoded copy, and a `format`
override wrote PNG bytes under a `.psd` name. The desktop's File › Save
already writes back only to layered files.
The rule now lives in one place, `engine::file_cmds::saves_in_place`
(plus `extension`), shared by File › Save, the control channel's
`app.save` and the headless save. Without `path`, a save writes back
only to a PSD, PSB or .pcraft file in its own format; anything else is
an error naming `path` and the file is left unchanged. An explicit
`path`, even the opened file's own, still writes as before. The MCP
`doc_save` description and `path` doc, and docs/control-protocol.md,
state the rule.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>