The base run comes from the file and is cloned once per character run, so
it now has to fit the template node budget, and each font is looked up
once per name. Also names the reopened-layer tuple in psd_kerning for
clippy::type_complexity.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Photoshop reads the character style from Txt2 and quit on the bare run
(`/0 0 /11 mode`) that new objects got. New and edited objects now get one
style run per character run, built on one of the object's own runs with
every key kept, and font, size, faux styles, leading, scales, tracking,
fill colour, OpenType keys and auto-kern mode set from the layer. A font
the block's font list lacks is appended to it. Edited objects therefore no
longer keep the old text's features by position.
build_tysh no longer writes TextIndex 0 for a layer without one: the
export took it as a kept number, so a Type tool layer landed on object 0
(a deleted layer's, or the one of a layer above it). set_text_index puts a
missing TextIndex right before EngineData, where Photoshop writes it.
pad_frames pads to at most MAX_TEXT_INDEX + 1 frames.
edited_text_keeps_the_files_txt2_object (#7220) now expects style runs
that follow the layer's runs and size.
Co-Authored-By: Claude Opus 5.5 (1M context) <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>
A new object kept the template's style runs, and PhotoCraft's reader takes
font features from Txt2, so a new type layer came back with the template
layer's features (salt, ss01). Its style runs are now generated like the
stub's; only the template's structure is copied.
New type layers were numbered past the kept TextIndex values only, so after
deleting the layer with the highest index a new layer landed on its object
and took its style. They are now numbered past the file's Txt2 objects too.
txt2_extras_survive_a_resave_of_unchanged_text now gives its layer the TySh
index a file's layer has; without one the layer is new and no longer lands
on object 0.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
On wasm32 a near-1 GiB .abr arrives as base64 and is copied several times
before decoding; with a 1 GiB sample budget the import could pass 4 GiB and
abort the tab instead of failing cleanly. Pick the decoder budget by pointer
width (like MAX_DECODED_BYTES) and derive the engine's file cap from it.
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>
new_object copied the template's rendered strikes, whose frame strike
(/99 /F) still named the template's frame in /10. It now names the new
object's frame, like the view's /0.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Text indices were assigned in one bottom-up pass, so a new layer took the
next free number before the layers above it claimed the numbers their TySh
keeps. Each of those was then renumbered onto its neighbour's Txt2 object
and took its style (70 of 74 layers on the #7698 file after adding a layer
at index 5). Kept numbers are now claimed first and new layers get numbers
past them.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Orientation buttons stored the turned settings from file.print.page
and then asked the driver again (driver_page) for the paper. If that
second query failed, the dialog kept the new settings with the old paper.
take_turned_page now takes both, and the orientation, from the one
answer; an answer without both leaves the dialog as it was and shows why.
The no-driver test used "00ff", which fails when the settings are read,
before any printer is asked, so it never tested the failed-driver path.
It now uses valid settings (a minimal DEVMODEW) and asserts the error
comes from the printer, not from reading the settings; with "00ff" it
fails. a_turn_takes_settings_and_paper_together covers the success path
and partial answers.
Reported by CodeRabbit in review.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Windows the Print dialog takes the paper and its orientation from the
printer driver, and had no way to change the orientation short of the
driver's own Print Settings… dialog. Photoshop has Layout buttons for it.
A print laid out landscape on portrait driver settings (from a script,
the CLI or another app) stopped with "the driver paper or orientation
differs from the print layout".
- The dialog has an Orientation row (Portrait, Landscape) under Print
Settings…. It turns the driver settings through file.print.page and
takes the paper from the driver again, so the paper and preview turn.
- file.print.page takes "orientation": "portrait|landscape" and returns
the turned settings as printerSettings, with the page they give.
- print_windows turns the settings (the given ones or the printer's
defaults) to the layout's orientation when the driver has the layout's
paper the other way round; another paper is still refused.
Turning sets dmOrientation and its dmFields bit in the DEVMODE and keeps
every other byte, the driver's private settings included; the driver
validates the result as it does settings from Print Settings…. No new
UI strings: "Orientation", "Portrait" and "Landscape" are in every
catalog.
Tests: the turned settings (orientation set, all else kept, with and
without private driver bytes), when a paper counts as turned, an unknown
orientation refused before any printer is asked, the dialog's reading of
the driver paper and its error path, and, run by hand on Windows, a
real "Microsoft Print to PDF" driver turning for a landscape layout.
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>