Radial Blur prepares its sample transforms once per filter (blur::radial::Plan) and shares them across output tiles, reading each bilinear tap once with channels kept together. Output is identical to the previous sampler at Draft, Good and Best, across colour models and depths (tests compare against the unchanged reference path); a failed table allocation falls back to that sampler. Measured on 24 MP RGBA8, Good, amount 1: Spin 66.1 -> 16.4 s, Zoom 19.2 -> 5.4 s (bench_radial example, docs/radial-blur-performance.md).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes#1833. Control and MCP may run Edit Contents, Convert to Layers and Save Contents on smart objects whose source is in memory (embedded, or a Photoshop UUID backed by embedded liFD data). Before any disk read the engine re-checks the source's stored path through the session's authorize gate, including nested sources re-rendered while unpacking, so external linked files stay refused; explicit layer targets work with another layer selected, and interactive linked-file access is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RAF's header, metadata records and Fuji IFD give size, packing (16-bit, 12-bit LSB-first,
14-bit in 32-bit words), layout, black, WB, crop and exposure bias; X-Trans and other
periodic non-Bayer CFAs go through a new edge-directed demosaic. Compressed and early
SuperCCD RAF stay unsupported (preview fallback). Tests use synthetic files only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The document tab's context menu gains Reveal in Finder (a new file.revealInFinder command that reuses the smart-object reveal helper and targets the clicked tab's document), and dragging a tab sideways reorders the open documents through document.move, with a drop line, in both tab strips. Flips UI-217-6 to done.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ui.inspect lists the open shell dialog (Workspace, Pixel Aspect Ratio, Show Extras,
32-bit Preview) with the id "shell", and ui.dialog.set/confirm/cancel accept it;
confirm goes through the automation authorizer and the dialog's shared OK. Fixes#1004.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Outside the crop frame the pointer is Photoshop's curved turn cursor and a drag turns the frame
about its centre (Shift: 15 degree steps) with an angle readout; handles resize along the turned
axes and a double-click inside commits. image.crop takes an optional angle: Image Rotation >
Arbitrary so the frame is upright, then the crop, in one undo step. Fixes#1792.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
menus::invoke records each successfully run command (deduplicated, newest first, last 8) and the palette lists them when its query is empty, falling back to the full list in a fresh session. Flips UI-217-7 to done.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A host lists fonts in fonts/manifest.txt beside index.html (craft-fonts' manifest format, paths
kept inside the site); the web shell lists those families in the font menus and fetches a family
when it is picked or a layout needs it, then re-renders the affected type layers without a history
step. No manifest changes nothing; package.sh copies one in with PHOTOCRAFT_WEB_FONTS_DIR. The
style menu also lists the weights of a variable font's wght axis.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
As in Photoshop CC 2019 and later, dragging an edge handle now scales both axes by the same
factor about the middle of the box (Alt: about the reference point), and Shift stretches the
one axis. Use Legacy Free Transform restores the old behaviour for edges as well as corners.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
step_damage pairs layers by id and steps over a layer only one side has when it draws nothing
(an empty pixel layer or group in Normal/Pass Through, no masks, effects or Advanced Blending,
not a clipping base), so New Layer, New Group and their undo/redo report no damage. Adds the
P51 perf scenario (new layer in the 150-layer document). Fixes#1771.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A complete Dutch catalog (nl.tsv) registered with the complete-menu languages (nl-NL and nl-BE
resolve to it) and in the coverage test, with the strings added on main since the PR opened and
Dutch bit-depth labels (8-bits, 16-bits).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A complete Ukrainian catalog (uk.tsv) registered with the complete-menu languages, with
one/few/many plurals (teens use many), uk-UA locale resolution, and the coverage test entry;
plus the strings added on main since the PR opened.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Open dialogs (File > Open and the single-file pickers for Place, scripts, notes and presets) now carry the most recent file's folder as initial_dir, and the desktop shell hands it to rfd's set_directory. Flips UI-217-3 to done.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Edit › Preferences › General takes ⌘K and Edit › Search moves to ⌘F, as in Photoshop. Nothing
else was bound to ⌘F (Filter › Last Filter stays ⌘⌥F); the shortcut-capture tests follow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Saturation slider's track runs grey to the hue at full saturation and the Lightness track black, the hue, white; both recolour as the Hue slider moves, as in Photoshop. Flips UI-217-16 to done.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A cfg(test)-only test.panic command drives Session::execute into the catch_unwind
guard in jobs::dispatch; the test asserts an internal-error Err, an untouched document
and history, and a working session afterwards. REL-3 flips to done.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
BUG-205-1/2/3 (5a5facc, #242) and BUG-207-1/2/3 (e2444ce, #234) flip to done;
each row now names the test that covers it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
UI-217-2 flips to done: b07a2e5b (#587) persists the window and dock width via eframe,
and window_and_panel_geometry_survive_a_restart asserts the round trip.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
document.inspect (and photocraft-cli info) now gives every smart object a
smartSource: embedded with its file name, or linked with a file path or,
for a PSD placed layer, the Idnt uuid its duplicates share. A script can
then tell duplicates of one placed file from independent contents, e.g.
to check that every location in a mockup template has its own art.
Co-authored-by: Juan Manuel Verges Solanas <jmverges@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Double-clicking the MSI now opens WixUI_InstallDir without the licence page:
Welcome, install folder (remembered for upgrades in
HKLM\Software\PhotoCraft\InstallDir), Ready, progress, and a Finish page with
a ticked "Launch PhotoCraft" box (WixShellExec, unelevated). The pages name
the full version, pre-release tag included (photocraft.en-us.wxl).
package.ps1 draws the banner and side bitmaps from the app icon, so no WiX
stock art ships. msiexec /qn still installs silently. The release and ARM64
workflows install the WiX UI and Util extensions; packaging-lint checks the
.wxl. Ported from VectorCraft (storytold/vectorcraft@aa548d2).
Claude-Session: https://claude.ai/code/session_01AP7ThRN3kpNmjsuJUerJui
Co-authored-by: Claude <noreply@anthropic.com>
Indexed Color accepted fractional colour counts and computed its live preview on the UI thread.
Counts are now whole numbers in 2-256 with a linear slider snapping at 8/16/32/64/128/256, and on
native builds the preview runs on one background worker that coalesces slider drags and rejects
stale results. Nearest-colour lookup uses an exact kd-tree above 64 entries (identical indices,
256 colours at 4.2 MP: 3018 -> 1061 ms). Web and inline sessions stay synchronous.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A new filter that removes a chosen colour by unmixing it into transparency, as GIMP's and
Krita's Color to Alpha do: per pixel the most transparent opacity that keeps the unmixed colour
in range, shaped by transparency and opacity thresholds. It runs through run_filter (selection,
smart filters, Last Filter, one history step), floats the Background first and refuses a
transparency-locked layer; the filter dialog gains a colour parameter. Fixes#1576.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The MCP server follows the crafts' shared conventions (FilmCraft #28): core tools
incl. render_preview, titles and annotation hints, strict argument keys (-32602),
live photocraft://document and photocraft://commands resources, and -32700
recovery from malformed JSON lines. Headless Render Video writes through the
granted write root, reports progress, stays ping-responsive and can be cancelled,
removing only the files it created. docs/mcp.md describes it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ui.inspect now has brushPicker: null while closed, {pos, list} while the Brush
Preset picker is open (list is its collapsed/filter/view/renaming state), so
agents can see what a right-click with a painting tool opened. Fixes#1558.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Windows and Linux the System title bar checkbox now carries the existing "Applies at next
launch." note, since saving it does not change the current window. Fixes#1661.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When one band failed, its siblings stopped with a generic "another band failed"
error, which could win the collection order and hide the real cause. The abort
flag now carries the first error, so every band reports it; tests cover traps,
error codes and the instruction budget across parallel bands.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Document the MSVC toolchain and C++ build tools workload, and how to diagnose
Smart App Control blocking Cargo or build outputs (os error 4551), with a
pointer to the packaged release. Fixes#1585.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The PHOTOCRAFT_AUTOMATION_*_ROOT variables are read only by the desktop app, so
headless photocraft-cli mcp/serve need the --automation-*-root flags. Say so,
and pass explicit roots in the .mcp.json and claude mcp add examples.
Fixes#1551.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ui.set gains rotation, gradientBlendMode and gradientClassic in its signature
and the stale duplicate ui.set line goes; ui.menu.invoke documents params,
ui.pointer tool, app.save an optional path, and serve's doc.save tiffLayers.
Fixes#1550.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Preferences entry said ui.dialog.apply/confirm save the dialog, but the
automation policy refuses whole-preferences updates (SECURITY.md). Document the
refusal, what happens to the dialog, and prefs.set for single safe preferences.
Fixes#1555.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Eyedropper options bar now has Photoshop's Sample Size (Point Sample,
3 by 3 ... 101 by 101 Average), Sample (Current Layer, Current & Below,
All Layers, All Layers No Adjustments, Current & Below No Adjustments)
and Show Sampling Ring.
- engine: new `document.sampleColor {x, y, size, sampleLayer}`
(sample_cmds.rs). Averages the square centred on the pixel, clipped to
the canvas, alpha-weighted, in the document's colour model (Gray, Lab;
CMYK documents use their composite RGB, since the profile round trip
is not exact) and rounded to the document's bit depth.
- ui: eyedropper_ui.rs options bar; state.rs ToolOptions
eyedropper_size / eyedropper_sample / eyedropper_ring (serde). The
size is shared with the painting tools' Alt-click eyedropper, the
Curves / Color Picker eyedroppers and the Info panel readout.
- control: ui.set eyedropperSampleSize / eyedropperSample /
eyedropperRing; documented in control-protocol.md and the MCP schema.
- i18n: new strings in all 14 catalogs (plus the missing Russian hint
the tl! scan had not reached).
- scorecard: TOOL-1649-1.
The brush was one global session setting (only its smoothing was per tool), so
switching tools carried the size, hardness, mode, opacity, flow and dynamics
over. sync_tool_smoothing becomes sync_tool_brush: it saves the session brush
for the outgoing tool and loads the new tool's on every switch. UiState's
smoothing_tool/tool_smoothing become brush_tool/tool_brushes (serde default, so
saved UI state still loads), and the session's own fields (colours, erase flag,
stroke seed) are never carried across a swap.
Called from the frame start, the options bar before the brush chip, both canvas
stroke paths, ui.set {tool} (so tool + brushSize in one call sets the new tool's
size) and select_tool_preset. A tool seen for the first time inherits the brush
in hand at Photoshop's 10 % smoothing.
Tests: each_tool_keeps_its_own_brush, the renamed per-tool smoothing test,
per-tool coverage in the ui.set brushSize test, and pencil_tests switching tools
before setting the smoothing as the options bar does.
From the review on storytold/photocraft#1607:
- ATTRIBUTION.md gets a row for corpus/affinity: the four upstreams at
their pinned commits, with authors and licences (CC0-1.0, and MIT by
Nick Beeuwsaert, Jeremy Cantu and DAIKI).
- The README said 176 sample documents in the provenance and 189 in the
limits. It now says 189 distinct public documents in both places; the
reader opens all of them (298 files with duplicates, no failures).
- crates/affinity/ joins CRITICAL, so test-corpus --changed runs the
corpus when the reader changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQPh8UDgMyARR5Hat9UchC
The maintainers approved the new crate, its fuzz targets and the corpus
upstreams in principle (storytold/photocraft#1607), so this undoes the
interim opt-in: ci.yml fuzzes preview and import_preview again, corpus
--all fetches the Affinity set, and its test runs under the normal
corpus feature. The provenance section stays.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQPh8UDgMyARR5Hat9UchC
Headless::save recorded the saved revision only for .pcraft, so saving a
document back to its own PSD or PSB wrote the file correctly and still
left session.list reporting dirty: true. Automation clients that use the
flag could not tell a saved document from unsaved work.
The test is now "was this a layered document save" (PSD, PSB or .pcraft)
rather than "was this .pcraft". A flat export (PNG, JPEG, ...) stays a
copy: it leaves dirty and the document's path alone. The desktop app.save
path already recorded the revision for every format, so this only affects
the headless JSON-lines server and the MCP stdio server, which both route
through Headless::save.
Tests cover the reported sequence for both .psd and .psb -- create, reopen
the saved file, edit, then doc.save with no path -- plus a control that a
flat export is a copy. docs/control-protocol.md now states the outcome of
doc.save for the dirty flag and the document path.
Co-authored-by: Space Bunny Free <chasbigham@gmail.com>
Review on storytold/photocraft#1607 asked maintainers to decide on new
fuzz targets in ci.yml and four third-party corpus upstreams in
corpus --all. Until they do, neither touches CI: ci.yml is main's again
(the targets stay in crates/affinity/fuzz, run locally), and the
Affinity corpus is fetched only by corpus --affinity and tested with
--features corpus-affinity, so --all and test-corpus are unchanged.
The plan is to move those files into photocraft-corpus.
The crate README now spells out the provenance: no Affinity software,
afread's MIT description re-described without translating code, every
structure confirmed on public files (CRC-32 per entry, self-typed
fields), semantics fitted to Affinity's own thumbnails, and how the
sample documents were licensed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQPh8UDgMyARR5Hat9UchC
- #1599 tests that ui_set's description names every field ui.set accepts; #1611 adds
`rotation` to ui.set, so the description now names it.
- #1611's el.tsv row for "Pick screen color" duplicates the one #1618 added on main.
- docs/scorecard.md regenerated with cargo xtask scorecard after merging both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHgFEHEsefoQoBParH47JA
Steps can be deleted and dragged into a new order, live while recording. Playing an action
while recording stores one named actions.play call, and nested playback stops on a child
failure, rejects cycles and goes at most 16 levels deep. Right-click an action to give it
F1-F12 (saved in preferences). View > Fit on Screen (Cmd+0), Zoom In/Out and 100% record and
replay. New actions.move command; actions.delete takes a step.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Save Preset... in File > New stores width, height, resolution, mode, depth, background (with
its colour) and display units in preferences. They are listed under a Saved category, where a
preset can be selected or deleted. Adds document.presets.save/list/get/delete; get returns
params for file.new, which gains an optional backgroundColor. Bad saved entries are skipped one
at a time.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Marks TOOL-214-17 done and drops the now-read trackpad rotate preference
from the unread list. The Paint Bucket test mapping carries the view's
camera angle.