Found while testing crates/ui-egui/src/crop_size.rs:86-107 (format_length / normalize_length)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
DDS textures can now be saved, not just opened: uncompressed (BGRA, BGRX, L8, A8L8; lossless) or BC1/BC2/BC3 (DXT1/3/5) from a clean-room block encoder, with an optional box-filtered mip chain, in the legacy header. Save As and Save a Copy ask with DDS Options; scripts pass `ddsCompression`/`ddsMipmaps` (file commands, control `app.save`, RPC `doc.save`, MCP `doc_save`/`doc_export`), and the CLI takes `--dds-compression`/`--dds-mipmaps`. Targa and DDS answers travel together as `FormatChoices` through the Save As flow.
Review follow-ups:
- File › Save behind DDS or Targa Options records a plain Save with its answer, and replaying `file.save` applies it (#3531).
- The block encoder finds the principal axis from every channel, so a channel that varies on its own can't hide the others' variation.
- DDS joins the shared round-trip suite; the header is built before the output buffer is allocated.
- Enter on a focused Cancel in DDS, Targa or TIFF Options cancels instead of saving.
Adds .coderabbit.yaml so CodeRabbit no longer reviews or comments on PRs opened by dependabot[bot].
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The 0.11.0 release's FreeBSD job failed: copy_xattrs used rustix's
fsetxattr/flistxattr/fgetxattr, which rustix offers only on Linux,
Android and Apple systems. The real copy is now gated to those, and
other Unix systems get a no-op copy_xattrs, as Windows has. The xattr
tests are gated the same way; the Unix mode test now also checks that
no temporary file is left behind.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The layered TIFF writer let the PSD exporter pick PSB from its size estimate, which counts every
layer plane uncompressed. A document with many canvas-sized layers passes 2 GB there while its
real layer data is a small fraction of it (a 3000×3441 mockup with 180 layers: estimate ~2.57 GB,
real data ~111 MB), so tag 37724 got the PSB-style header ("Adobe Photoshop Document Data
V0002", 8-byte lengths). Photoshop does not read that in a TIFF: it opens only the composite, as
one locked Background.
PsdExportOptions gains `size_limit_psb` (default true, .psd saves unchanged). The layered TIFF
turns it off and writes PSD-style data ("Document Data Block"), falling back to PSB only past
30000 px, when forced, or when a length really overflows 32 bits.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
A turned crop rotated the whole canvas, then cropped to the frame. With
Delete Cropped Pixels on, every layer, mask and channel is now trimmed to
the frame's source (plus the interpolation kernel's reach) before the
turn, so the rotation costs the frame's size: a 512x442 frame turned 3°
on a 3859x2594 two-layer document goes from 541 ms to 42 ms.
Rotate Canvas also fills the Background with shared tiles instead of a
canvas-sized float buffer.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
In a Flatpak on Wayland, arboard can't open at all: the sandbox hides the
data-control protocol from it, and --socket=fallback-x11 gives no X server
while a compositor runs. Paste then reported "the clipboard holds no image"
even when it held one. Now the clipboard service reports that it had no
clipboard to read, and the paste shows a notice that explains it and gives
the XWayland command for this install.
Also adds my contributors/people.toml entry.
Claude-Session: https://claude.ai/code/session_01AbyXia7KGdFgbNjBeggY8J
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
With Ctrl (Cmd on the Mac) held over a guide, Photoshop's tools that turn Ctrl into
the Move tool show the guide cursor and drag the guide. Three gaps remained:
- The Frame and Artboard tools showed the guide cursor under Ctrl but took the
press themselves (a new frame or board), so the guide never moved. Their pointer
handlers checked the selected tool instead of the effective one.
- The Gradient tool's live mode took a Ctrl press sent with the event (automation,
`ui.pointer` with `command: true`) instead of handing it to the Move tool.
- With Window > Modifier Keys' latched Ctrl, the press took the guide but the
pointer kept the tool's own cursor: `command_guide_at`, the decision the cursor
and the guide context menu use, knew only the selection tools.
`command_guide_at` now covers every `hold_keys::cmd_moves` tool, and not during a
stroke or type editing. Frame, Artboard and live Gradient step aside for a Ctrl
press when they have no drag of their own under way. Tools that keep Ctrl for
themselves (Pen, Hand, Zoom, Crop, type, shapes, slices) and Lock Guides are
unchanged.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
One value of the wrong type in prefs.json (say "recentFileCount": "ten")
made the whole file fail to deserialize, so every preference fell back
to its default for the session, and since the file was never rewritten
it happened again on every launch.
load_prefs_json now reads leniently when the file doesn't deserialize as
a whole: it starts from the defaults and lays the file's values over
them one at a time, descending into section objects and map entries,
keeping every value that still reads and leaving out only the ones that
don't. Colour settings get the same treatment instead of resetting as a
whole. It returns the left-out values (keys and original value); text
that isn't a JSON object still fails as before.
On launch the app shows a notice naming the reset paths and saves once:
the three-way merge base gets the unreadable values, so the merge
replaces (or removes) exactly those in the stored file and keeps
everything else, including keys this version doesn't know.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Load Actions applied each rename as its own pass over every action in the set, so chained
renames ("A" -> "A 2", "A 2" -> "A 2 2") retargeted a step twice, and with replace:false it
also rewrote steps of actions already in the set. Now one original->new map is applied once
to the newly loaded actions' Play and Conditional (then/else) steps and to function keys.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
decode_into computed out.len() + expected unchecked. With a non-empty
buffer and an expected length near usize::MAX it panicked in debug builds
and, in release, wrapped and returned Ok having decoded nothing. Compute
the target with checked_add and report LimitExceeded, and test each packet
as len > target - out.len() so the bound checks cannot overflow either.
The only callers are decode and decode_rle in this module, and the crate
README documents packbits::{encode, decode} as the public API, so
decode_into becomes pub(crate).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Pressing Enter while the frame is still being dragged commits the crop; commit_crop now ends the drag, so the release no longer restores a frame in the old document's coordinates as a pending crop.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
image.revealAll ran image.crop, whose step is labelled "Crop". It now
runs it inside analysis_cmds::compound with the label "Reveal All".
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Save as PDF was only a free text field, so saving a PDF meant typing a
full path by hand. Browse… opens the standard Save dialog (pick_save),
suggesting the document's name as a PDF, and puts the chosen path in the
field. file_ui::body now gets the dialog id so the answer can be written
back into the Print dialog on a later frame.
Refs storytold/photocraft#5328
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
`to_press` and `control_point` read a point in the press-time view through
`remap`, which rounds it through f32 and the view even when the view hasn't
moved. A `ui.pointer` down + up at the same point then came back slightly
apart: the frame moved by a hair and a box Undo step was pushed that undid
nothing visible. Skip the remap while the view is still the press view.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* perf(codecs): Avoid redundant PNG filter passes
Keep the previous filter path as an exact byte oracle and measure the filter kernel independently of compression.
* docs(perf): measure PNG filtering on real images
* perf(codecs): stop PNG candidates at the winning cost
* Drop the local benchmark report
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>
* Home screen: fit the card and stack the buttons in a narrow window
The Home screen's card was a fixed 460 points wide, centred on the canvas, with two
190-point buttons side by side. A tiling window manager can give PhotoCraft a canvas
narrower than that (Hyprland on Omarchy gave a 728-point window, below the 760-point
minimum), so the New document and Open buttons were clipped at the edges.
The card is now as wide as the canvas when the canvas is narrower than 460 points. The
buttons are sized from their labels (so longer translations still fit) and are stacked
when two of them don't fit side by side; the card grows by one row when they stack.
Adds start_screen_buttons_stay_inside_a_narrow_canvas, which fails before this change
(New document starts at x = -1 in a 390-point canvas).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* contributors: add TaylorBurke
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Save Actions wrote these four steps under the four-char codes BoxB, SrfB,
Fltn and BlnW, which Photoshop does not run: executeAction fails with "the
command is not currently available" (measured in Photoshop on a document
with a filled pixel layer; Fltn resolves to the flatness property, not the
flatten event). The names boxblur, surfaceBlur and blackAndWhite and the
code FltI run.
- Box Blur, Surface Blur and Black & White now export by name
(boxblur, surfaceBlur, blackAndWhite); Flatten Image exports as FltI.
The descriptors are unchanged.
- Import accepts the names, FltI and flattenImage, and still accepts the
old codes (a LEGACY_CODES table consulted only for four-char codes), so
actions PhotoCraft saved before keep loading.
- Tests: each step exports the new id and imports back to the same command
and params under the new id, the name and the legacy code; a set mixing
Photoshop-shaped and legacy steps loads and re-saves with the new ids;
near-miss ids (case, padding, empty, NULs) stay Photoshop steps; broken
descriptors don't panic.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Explain the per-document memory limit and automatic GPU budget caps.
* Add catalog entries for the Memory Usage explanation (#6546).
The new preference strings need a row in every complete-menu language
for the i18n tests.
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* fix(engine): record Apply Layer Comp by the comp's name, not its document-local id (#7181)
* test(engine): a recorded Apply Layer Comp applies the comp of that name in another document (#7181)
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
One ToolOptions::sample_all_layers was shared by twelve tools, so ticking
it for the Magic Wand or Paint Bucket also turned it on for Blur, Sharpen
and Smudge. A dab of those on an annotation layer then pasted an opaque
copy of the composite there, which sat above the picture and hid every
later blur of it (the sharp round patch in #5524).
ToolOptions::sample_all is now a SampleAllLayers with one flag per tool,
all off by default: Magic Wand, Quick and Object Selection (and Object
Finder), Paint Bucket, Magic Eraser, Spot Healing, Patch, Content-Aware
Move, Remove, Blur, Sharpen and Smudge. Each options bar and command
reads its own tool's flag, and Reset Tool resets only that tool's. The
new key means older preferences drop the shared flag, so every tool
starts with it off.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Session::edit has reported a new layer's bounds as damage since #3847, so
Duplicate Layer, Duplicate Layers and Paste no longer need to keep the
document from before the edit and work the damage out again with
note_insert. Paste Into still does: it is three edits.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
W and H showing percent rejected a typed length ("40px", "2 in", "5cm"),
since the shared unit parser has no length to convert a percentage
against. The fields now carry the box's original side as the length 100 %
stands for, so a typed length becomes that share of it at the document's
resolution, and "50%" typed into a length view is half the original side.
The field keeps its chosen unit; the aspect link and the scale limits
apply as before. Other percent fields (opacity, fill) still reject lengths.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* GPU canvas: redraw mip levels of 128 px or less whole; AMD Vulkan corrupted them after partial updates (#6418)
On an RX 6900 XT with Vulkan (driver 26.8.1), a regenerate_mips pass that drew only the damaged
part of a small mip level corrupted texels outside its scissor, at the level's top-left; DX12 on
the same card was fine. A sweep of 15 canvas sizes x 8-bit/float x 6 dabs found 66 of 180 cases
corrupt on Vulkan, every corrupted level at most 64 px; sampling the levels in a shader shows the
same pixels, so it is the texture contents, not the readback.
mip_damage (regenerate_mips's damage arithmetic) now redraws levels of at most WHOLE_MIP_SIDE
(128 px) whole on every adapter: no new passes or textures, only a larger scissor on the
smallest levels. With it the sweep is clean on both backends.
Tests: small_mip_levels_are_redrawn_whole (the rule, no GPU) and
partial_updates_match_a_full_rebuild_at_every_mip_level (fails on the Radeon over Vulkan without
the change, passes with it, like canvas_mips_preserve_opaque_pixels_and_partial_updates).
* contributors: add 00xJS (GitHub profile name)
* Mip sweep test: hold the GPU lock like the other GPU tests (#6438 review)
---------
Co-authored-by: 00xJ0 <56056764+00xJ0@users.noreply.github.com>
* Hide the Hyphenate checkbox until layout actually hyphenates (#6710).
The flag still round-trips to PSD and ⌥⇧⌘H still toggles it, but the
Paragraph and Properties panels no longer advertise a no-op.
* Assert Hyphenate stays hidden until layout hyphenates (#6710).
The Paragraph properties and paragraph style options editor now have
tests that fail if the no-op control is shown again.
* Drop the duplicate #[test] on hyphenate_is_not_shown (#6710).
Clippy -D warnings failed on duplicate-macro-attributes and unused mut
on the harness that only queries the label.
* fix(linux): keep crash-recovery autosaves in $XDG_STATE_HOME, out of the config folder (#6612)
* test(linux): cover the XDG state recovery folder (#6612)
Migrate Presets deserialized the incoming tool presets all or nothing, so
one damaged entry dropped every good one, and it skipped the checks the
launch loader applies: blank or overlong names were imported and then lost
on restart, and a large file pushed the library past MAX_PRESETS. Read the
list with `presets::tools::load` and stop at MAX_PRESETS.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
While editing paragraph text, a click inside the box that missed the glyphs
(left of a line's start, or below the last line) committed the edit. Like
Photoshop, it now places the caret at the nearest position; a click outside
the box still commits.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
transform_doc_marks moved guide positions but not their colour entries,
which are keyed by exact position: after a crop the coloured guide fell
back to the preference colour and a guide that landed on its old place
took the colour. Map the entries with the same expressions as the
guides (swap orientation on quarter turns, clear on arbitrary angles).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* perf(photomerge): build multi-band bands in bounded batches (#7758)
panorama::multiband built every source's Laplacian and weight pyramids
at once and kept them all until accumulation, so Photomerge's peak
memory grew with the image count (~5 GB for four 24 MP sources). Build
the bands in batches under a 2 GiB estimate (at least one image) and
add each batch before building the next. Each pixel still adds the
bands in image order, so the output is bit-identical.
Planes now move into the pyramids instead of being cloned, and the
coverage and push-pull masks are freed once used: with one image built
at a time those clones were serial passes. seam_blend shifts its ROIs
in place instead of cloning every source for the blend.
Peak during the blend: 4x24 MP 5.04 -> 2.81 GB, 8x12 MP 5.50 -> 2.61 GB,
at +5% time (2x24 MP unchanged). New example bench_multiband measures it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* perf(photomerge): let up to four 24 MP sources build their bands together
A 2 GiB band budget built every 24 MP source alone, which made the usual
2-4 image Photomerge 12-36% slower on a 32-thread desktop. The native
budget is now 5 GiB, so those still build side by side and only larger
stacks are batched. wasm32 has no threads, so it adds each band as soon
as it is built.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Photoshop's Action Options pair the function key with Shift and Command
checkboxes. The Actions row menu offered plain F-keys only, and .atn import
dropped any key with a modifier. The row menu gets Shift and Command (Ctrl off
the Mac) toggles, and .atn load and save keep the modifiers.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
select_tool_preset only merged the preset's tool options, so a crop
preset's ratio was set without refitting the frame; the options bar's
dropdown already refits. It now compares the ratio before and after and
calls crop_ui::apply_ratio when it changed, as the dropdown does.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>