- #8714: a Camera Raw Filter step imports only with a process version
the PSD mapping has verified (PrVN/PrVe 5/184549376 or 6/251920384)
and a CrVe text; other steps stay kept steps that save back unchanged.
- #7642: the patternName fallback matches names only, so a pattern
whose id equals that text no longer wins. A step that names its
pattern only by patternName exports that name.
- #8327: the iso-2022-jp label is refused (its names are 7-bit escape
sequences that would pass through as ASCII).
- #10417: the Levels export gate also refuses endpoints of the wrong
shape (both ends text, a text gamma) and channels that aren't objects.
- #10459: a set paused at a step dialog keeps the rest of the set by
action name, so actions.move while the dialog is open neither plays a
foreign action nor skips one; "Played N steps" covers the whole Play
(steps before the pause and the dialog's own step); the suggested .atn
name is cut to 255 bytes at a character boundary; actions.list returns
every set, empty ones too; set_names dedups with a HashSet.
Co-authored-by: FinnNk <1424666+FinnNk@users.noreply.github.com>
Co-authored-by: Silvertree2010 <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: regexer <3689390+regexer@users.noreply.github.com>
Co-authored-by: marshiyar <57055687+marshiyar@users.noreply.github.com>
Co-authored-by: hectorviov <1501713+hectorviov@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Stream PSD user and real masks into tile rows
* Correct streamed-mask fixture metadata and test lint issues
* Budget PSD pattern expansion and bound multichannel conversion
---------
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
The previous fix-up counted a letter glued to an ASCII letter as common
only when it was Latin. That turned ordinary windows-1251 names such as
"CMYKканал" or Ukrainian "Копiя" (Latin i) into a windows-1252 (or GBK)
reading of the whole set, with no warning, where the PR read them right.
Back to the PR's scoring for single-byte pages; the ¥ removal stays.
windows-1252 "Fläche m²" / "Preis £5" reading as windows-1251 is the
PR's own known limitation, now noted in the code. A Russian set plus
"CMYKканал" is pinned as windows-1251.
Co-authored-by: regexer <3689390+regexer@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mac Roman 0xA5 is the bullet, windows-1252 0xA5 is ¥: with ¥ counted in
the windows-1252 re-weigh, Mac "Ebene • Kopie" read as "Ebene ¥ Kopie".
Drop ¥; the other kept symbols are no letter or typographic mark in Mac
Roman.
windows-1251 has І і д where windows-1252 has ² ³ ä, so "Fläche m²" read
as "Flдche mІ" (already so in the PR head; main showed it right). In a
single-byte page, a letter glued to an ASCII letter now counts as common
only when it is a Latin letter. Real Cyrillic names ("Новый слой",
"Копия слоя 2", "Канал RGB") still read as windows-1251. "Preis £5"
(Ј between a space and a digit) still reads as windows-1251; documented.
Co-authored-by: regexer <3689390+regexer@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The previous fix-up put main's atn fuzz target back. It compares the
parsed actions with assert_eq!, so any valid file holding a NaN double
fails the fuzzer falsely (NaN != NaN); the PR's own contract test
nan_payloads_signed_zero_and_infinities_survive_without_false_fuzz_failures
covers exactly that. The target now calls the shared check again, at its
new path under tests/it/common.
Co-authored-by: denstu473 <63150090+denstu473@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The contract tests no longer compiled on main (AtnItem::dictionary is now
Vec<u8>, so it is compared with b"Future event"), sat outside main's
tests/it layout, and the new atn.yml added six legs (two macOS) to every
PR against ci.yml's policy. Move the tests into tests/it so the existing
CI runs them, drop atn.yml and docs/atn-testing.md, keep main's ci.yml and
fuzz target, and note the committed fixtures in ATTRIBUTION.md.
Co-authored-by: denstu473 <63150090+denstu473@users.noreply.github.com>
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)
Conflicts with main's batch #9880:
- psd file.rs / layer.rs: #5287 (PSB by encoded size, RLE tables widened on
promotion) against #6583's lazy reader. Main's new widening and length code
read `.data`, which #6583 leaves empty while the bytes stay in the source;
it now goes through `encoded()` / `encoded_len()` (also the auto-merged
lines in `rle_widening`, `encoded_len` and `LayerRecord::write`), and the
lazy reader sets `write_as_psb: false`. New test
`a_lazy_psd_promotes_to_the_same_psb_as_an_eager_one`: a lazily read PSD
writes the same PSB bytes as an eager one; it fails with a `.data` read.
- direct_select.rs / vector_ui.rs: #9321's Type Path and #6353's hidden
Work Path both kept.
- io tests main.rs: both modules. scorecard.md regenerated (12 of 181);
ui-parity.md and gaps.md follow it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Merged at the fix-up commit before its main merge (362947695d); the
conflicts with the batch are resolved here: psd_import.rs keeps both
sides' tests, tiff_layers.rs takes ImageData::new.
The lazy parse took any error from read_layer_info on a Layr/Lr16/Lr32
global block as a damaged block and dropped the layers. A failed read of
the source (PsdError::Read: a dropped network share, a file rewritten
while it opens) then let a 16/32-bit layered file open silently from its
merged image, with no damage warning, and a save replaced its layers.
A read error now fails the parse, as reading the whole file would.
The new test fails each read of a clean parse once, in turn, and checks
the parse either fails with the read error or matches from_bytes; with
the fix reverted, read 20 of the Lr16 sample drops the layers silently.
Found by CodeRabbit's review of the PR.
Co-authored-by: ptrinh <774083+ptrinh@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Opening a 2.14 GB PSD (2000×2000, 109 layers, 821 MB of embedded smart objects) peaked at
7.28 GB and took 6.8 s; it now peaks at 3.00 GB and takes 3.6 s (macOS peak memory footprint,
`cargo run --release -p photocraft-io --example psd_open_mem`).
- photocraft-psd: `PsdFile::from_source` parses from a `ReadAt` source (`FileSource`: pread /
seek_read in bounded reads). Channel data and the merged image become `Stored` ranges read
when decoded; the model equals `from_bytes`'s and writes back the same bytes (tests compare
the two on every generated file, including Lr16/Lr32 layer blocks and truncated files).
- `ChannelData::row_decoder` decodes any band of rows: RLE via its row table (a stored channel
reads only the band's bytes), raw by offset, ZIP inflated once.
- photocraft-io: `import_file` opens a path; PSD/PSB through `from_source`, other formats read
whole. Layers decode in bands of whole tile rows (~32 MB) into tiles: no whole-layer plane or
interleaved buffer. Global blocks move into the document instead of being copied; finding an
embedded smart object no longer copies every file in `lnk2`.
- photocraft-raster: `Surface::share_uniform_tiles` + `TileInterner`; the PSD import shares
single-value tiles (backgrounds, fill caches, masks) across layers.
- The background open job and the CLI/MCP (`automation::files::open`) use `import_file`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merged with our fix-up branch pr-8885-fixup (8f6863f53) on the author's head 996b55eb4.
Conflicts resolved:
- crates/io/src/tiff_layers.rs: #9148's tiff_source (an empty LMsk gets Photoshop's 14 bytes)
gives way to #8885's tiff_image_source, which writes the same bytes for an empty or short
LMsk; #9148's test stays.
- crates/io/src/flat.rs: #8776's DDS alpha step and #8885's TIFF channel stamp both run.
- crates/io/Cargo.toml, Cargo.lock: #8402's zune dev-deps and #8885's tiff dev-dep.
- crates/io/tests/it/tiff_layers.rs, docs: unions; docs/scorecard.md kept as is, regenerated at
the batch gate.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
psd_import.rs keeps main's warning for an incomplete merged extra channel
next to the PR's Photoshop spot polarity. people.toml takes main's entries
plus jasallen's own. Docs take both sides' status lines and revision rows;
docs/scorecard.md is regenerated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(engine): defined and imported patterns survive a restart (#9317)
The pattern library lived only in memory: patterns defined with Edit >
Define Pattern or imported from a .pat file were gone after a restart,
while their Patterns panel folders (saved with the preferences) still
named them. Photoshop keeps them.
The preset store now writes the user patterns (the library minus the
built-ins, in order) to patterns.pat, Photoshop's own pattern file
format, whenever their ids, names, sizes or order change, and loads
them when the store attaches. Like actions.json, a file that fails to
load is warned about and never overwritten. The web store accepts the
new record.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Patterns: persist same-id pixel replacements, retry a failed removal
CodeRabbit (discussion_r4369506324, r4369506326):
- an imported .pat whose id is already in the library replaces its
pixels, which the id/name/size key didn't show, so the old pixels came
back after a restart; PatternLibrary::pixels_rev is bumped on such a
replacement and is part of the key;
- a failed patterns.pat removal recorded the empty library as saved and
was never retried, so the deleted pattern came back; it now stays
pending, as a failed write does.
Tests: a_same_id_pattern_replacement_survives_a_restart,
a_failed_patterns_removal_is_retried.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Patterns: persist replaced built-ins and ASL replacements
CodeRabbit (discussion_r4369536978, r4369536981):
- a .pat import that replaced a built-in pattern by id was filtered out
as a built-in and never saved; only built-ins exactly as shipped
(is_pristine_builtin) are skipped now, and on load a saved built-in
takes the shipped one's place unless it changed before attachment;
- style.presets.importAsl replaced patterns by id without bumping
pixels_rev; both imports now go through PatternLibrary::upsert.
Tests: an_imported_builtin_replacement_survives_a_restart,
an_asl_pattern_replacement_bumps_pixels_rev.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* .pat: refuse names and counts the reader won't load, on write too
CodeRabbit (review 5641404908, outside-diff, two findings): now that the
preset store writes the user patterns, a file its own loader rejects or
truncates loses patterns at the next start.
- a name of 65,536+ UTF-16 units (with its NUL) was written although the
reader refuses it; write_pattern refuses it now;
- more than 100,000 patterns were written, and the reader kept the first
100,000 without a word; both now refuse counts over MAX_PAT_PATTERNS.
Tests: a_name_the_reader_refuses_is_refused_on_write,
more_patterns_than_a_pat_file_holds_are_refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Patterns: an early same-id import with other pixels is saved
CodeRabbit (review 5641454449, outside-diff, preset_store.rs:1191-1202):
a pattern imported before the store finished loading, with the id, name
and size of a saved one but other pixels, was appended (no pixels_rev
bump), won the merge, and then matched the loaded file's key, so it was
never written and a restart brought the old pixels back. The merge now
bumps pixels_rev when the copy it keeps differs from the saved one.
Test: an_early_same_id_import_wins_and_is_saved.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* PSD: write pattern transparency where Photoshop keeps it (#8054)
A pattern defined over transparency (delete the Background, Define
Pattern, then a Pattern Fill layer from it) was saved with its alpha
plane in the virtual memory array list's user mask slot (24), and
Photoshop refused the PSD. Photoshop keeps pattern transparency in the
last (sheet mask) slot and leaves the user mask unwritten (psd-tools
multichannel-pattern-fill.psd, written by Photoshop CC 2019). The same
writer serves `Patt`/`Pat2`/`Pat3`, `.pat` and `.abr`.
Files saved by older builds still open (the reader takes the first
written array after the colour channels as transparency), and saving
them again rewrites their pattern block instead of keeping the raw
bytes, which `block_has_user_mask` detects.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PSD: rewrite only legacy pattern blocks, keep unreadable ones (#8054)
When the document's patterns are unchanged, a pattern block with
transparency in the user mask array is now rewritten in place from its
own patterns, and every other block (pattern blocks that fail to decode
included) is kept verbatim. Previously a legacy block forced the
replace-all path, which dropped an unreadable pattern block that an
unchanged save used to keep. Blocks are decoded in order with one shared
budget, as from_global_blocks does.
Addresses CodeRabbit review on #8756.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Brings split/tiff-extra-samples-codecs at e6fca054, which merged main
1695f323. Conflicts:
- crates/io/src/lib.rs: main moved PSD import into import_psd(); it now
passes PsdImportOptions::default().
- PsdExportOptions (psd_export.rs, tiff_layers.rs): main's new
read_linked_files next to this branch's photoshop_spot_polarity.
- docs: main's newer status lines and rows, with this branch's TIFF
statements re-applied (dated 2026-10-23). docs/scorecard.md is main's
and is regenerated after PR 2 is merged too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolves docs/file-format-parity.md: main's newer status line and
revision rows, with this branch's MTrn entry on top (dated 2026-10-23).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main merged #5940, #6472, #7838 and #7922 directly. Kept from the batch:
the snapshot/History-follow identity at the history limit (#5940,
c7ef3676a; main still had the 3-tuple HistorySpot) and the strokeless
shape at a 0 px width step (#6472, 962539607; main still added the
stroke). Main already carries the other two fix-ups (34c9d0ea4, dff8bfd0e).
Overlaps with main resolved:
- #5842 vs main's #5568: one path, apply_color, tool-gated as #5842
has it, with #5568's float fill, kept fill opacity and lock skip;
shape_fill_ui's duplicate foreground_changed/gesture dropped; #5568's
tests run with a shape tool.
- #6230 vs main's #5180: one Type tool path click (path_target), no
second type.attachToPath; vertical type does not start type on a path,
as main has it; main's path_at/path_cursor and canvas arm dropped.
- #5452 moved the shape swatch to shape_paint_ui.rs: main's hex field
(#6201) ported there.
- type_extra_cmds::with_text takes main's dpi form, pub(crate) for
type_area_cmds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- gpu: the test modules share one GPU_LOCK, and timing and vector_mask
take it too, so one process drives one wgpu device at a time.
- ui-egui: the wgpu tests in pattern_preview_canvas, adjust_preview_gpu,
marquee_autoscroll and layout_perf take gpu_lock().
- affinity compound_limits: the child process selects its test by the
module path (compound_limits::<name>); with --exact, the bare name
matches no test and the child passes without running the check.
- xtask scorecard reads crates/io/tests/it/corpus.rs.
- Docs and comments name the new test paths and the --test it filters.
Every tests/*.rs file is its own executable that links the whole crate
graph. Move each crate's test files into tests/it/ as modules of one
tests/it/main.rs; helper modules under tests/common move with them.
These keep their own binary (own process):
- engine panic_hunt: a process-wide panic hook counts every panic.
- engine text_gamma: changes the process-wide text gamma.
- engine smart_filter_stages, mask_cache_purge: assert on global caches.
- codecs pnm_peak_memory: reads the process's peak RSS.
- photocraft mac_menu_appkit: harness = false.
crates/engine/src/file_cmds.rs: batch keeps the save-point closure (takes the output path) and also copies the session's authorize hook into each scratch session, as main now does.
crates/raw/src/sensor.rs: both sides cap the memory of retained tiles; kept the existing retained_segment_bytes check (checked multiplication, same limit) and dropped main's duplicate inline loop.
FilterEffects::parse tested the trailing bytes with
rest.iter().all(|b| b == 0) before checking rest.len() <= 3, so every
loop iteration scanned the whole remaining buffer. A block with many
empty sections produced quadratic CPU work.
Reorder the short-circuiting && so the three-byte length check gates the
zero scan, making each padding check constant work. Behavior is
otherwise identical.
Finding: 4887ae49-cfda-43b0-9296-a1816fc1ddba (CWE-407)