* Raster: ask the allocator whether a big operation fits; PSD save reserves its whole output
New photocraft_raster::headroom: ensure(need) tries to reserve the need plus a margin and frees it
at once (web only; native always passes), below(bytes) tells whether less than that can be
allocated, and simulate_available fakes the answer for tests and diagnostics.
PsdFile::to_bytes reserved only the pixel data, so embedded smart-object data and other tagged
blocks grew the output by doubling, which on the web is one unrecoverable huge realloc. It now
reserves the exact encoded length.
* Big operations that won't fit in memory fail with an error instead of trapping
Place, Image Size, a smart-object re-render and Save now ask the allocator whether their estimated
need can be allocated before they start (photocraft_engine::memory_gate, estimates per operation,
photocraft_io::estimate_export_bytes for saves). A refusal changes nothing and says what the
operation needed and what to do (save, Edit > Purge > All, close documents).
A standing notice warns when less than 512 MiB can be allocated, with hysteresis, so the user can
save before anything traps. Notices can now be sticky, which keeps them past the auto-hide delay.
* Web build: a trap says so instead of freezing the page, and ?free-mb simulates low memory
index.html holds a hidden, static 'PhotoCraft stopped' message with a Reload link. web.rs reveals
it when the module traps (the window error event, or an unhandled rejection from a spawned
future), tells running out of memory from an internal error by the panic message a chained panic
hook kept, and skips the Leave site prompt once stopped. ?free-mb=N makes the memory gates and the
low-memory warning see only N MiB, for checking them in a browser.
* Docs: web build memory handling in the gaps list and the parity assessment
* Engine: use is_multiple_of in the megabyte formatter
* Cargo.lock: the web shell depends on photocraft-raster
* Web boot page: carry the stopped message too, as the test that keeps the two pages equal requires
* Save check counts one copy of the document, and runs once per save
The encoded file's buffer is reserved fallibly, so a save it doesn't fit
in fails with an error instead of trapping; counting it as well refused
saves that would have fitted. On the background-save path the check ran
in write_document and again in start_save, which other callers reach
directly; it now runs only in start_save there.
* Review fixes: warning check allocates nothing, PSD merged image counted, automation saves checked
The low-memory warning's periodic check reads the size of linear memory
instead of reserving a block: wasm memory never shrinks, so a trial every
few seconds raised the high-water mark by up to 768 MiB for nothing. The
operation checks keep their trial reservation.
PSD and PSB saves always build a canvas-sized merged image (a placeholder
without maximum compatibility) and hold it raw and encoded; the save
estimate now counts it. The synchronous automation save checks memory
before encoding, as File > Save does, and its test asserts the memory
error.
The trap listeners go in before the app is built, so a trap while
building it shows the stopped message too, and a memory access out of
bounds reads as unexpected rather than as running out of memory.
* Cover layered PSD preflight limitation
* Fix layered PSD test iteration
* Docs: layered PSD preflight limitation in the gaps list
* Web OOM gate: gate direct exports, truthful advice for unbounded PSD sources (#10024)
* Web OOM gate: estimate layered PSDs per layer instead of refusing every real PSD
The PSD check admitted a file only when its color-mode data, image
resources and layer section were all empty. Every Photoshop file has
image resources (at least the resolution), so on the web nearly every
real PSD was refused for Place, PSD smart objects could not render,
and Image Size was refused for documents holding one, whatever the
free memory (u64::MAX never fits).
The color-mode data and image resources hold no pixels, so they are
now only skipped. A layered file reads its layer count from the layer
info (signed, absolute value) and adds a canvas-sized surface and mask
per layer to the flat estimate. Files that are not 8-bit RGB or
grayscale, keep their layers in a Layr tagged block, or whose sections
can't be read stay unbounded and refused, as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Web stopped page: match trap messages ignoring case
Safari reports the allocation-failure abort as "Unreachable code
should not be executed" and Firefox says "out of memory", which the
case-sensitive matching missed: the trap was not recognised, or was
shown as an unexpected stop instead of running out of memory.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Export estimate: count the merged image OpenRaster writes
The ORA writer keeps the layers but also flattens the document for
mergedimage.png, a 16 B/px composite of the canvas, which the
estimate left out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Engine: droplet dependency capture skips disabled steps; a nested Batch
that was cancelled or could not write its error log fails the caller;
runDroplet forwards overrideOpen; actions.rename follows named Batch
steps; Batch steps inside a script count against MAX_SCRIPT_STEPS;
output claims compare destination identity; Save and Close never
replaces a read-only source. ATN: Md must be a BlnM enumeration,
adjustment-layer names must round-trip, Calculations exports a missing
blending as Multiply, Apply Image refuses unnamed-layer transparency.
Filters: Shape Blur records its resolved geometry, reserves its kernel
memory first, and reports a preparation failure as an error, not a
cancel; Crop keeps an artboard preset when the size is unchanged and
fits the board on a turned crop.
Codecs/cms/algo: zero-axis FITS image HDUs are empty; a GIF walk that
breaks off before the trailer fails closed; two-value .3dl shaper rows
are applied; fragmented custom kernels are refused and their work polls
cancellation.
UI: Conditional branches play through panel playback; paused actions
refuse to resume after being replaced; Playback Options OK validates
its fields; transform double-click ignores a hidden pivot; path boxes
snap to a stationary selection; modal blocking uses the effective tool;
a picker-consumed release clears snap lines and ends a Move drag; the
Crop preset control shows Selection and checks only it; Crop box undo
restores frame_from_selection; Bosnian fill labels.
Tests: timing asserts moved to an ignored test, plus the review's
coverage nitpicks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The position checks of the codepage guesser (letters after a character, a closing !,
the Cyrillic run around a letter) rescanned the rest of the name from every non-ASCII
character, so a hostile .atn or PSD with a 64 KiB step name of one letter kept Load
Actions busy for about three minutes. Each is now found once per name; the three
measured cases take 0.03 s together, and a regression test covers them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review: Mac Roman "90°" alone has no ASCII letter, so its one windows-1251 letter made the name
"mostly Cyrillic" and the fallback showed "90Ў"; "¡Ánimo!" counted nothing for windows-1252 since
only ASCII letters were counted after ¡; and Mac Roman "20 °Celsius" tied with windows-1252
"20 ¡Celsius". A lone Cyrillic letter right after a digit never counts (a run after a digit, "5шт",
still does); the letters after a character include accented ones; and ¡ counts where it opens an
exclamation, two letters or more after it and the closing "!" later in the name, which the degree
sign before a unit never has.
Re-lands 47ee3f696, which went out under another PR's message by mistake and is reverted just
before this, with one test case less: without any hint a lone Mac Roman degree sign before an ASCII
letter is also a Big5 punctuation pair, a tie the fallback flags, so the Mac Roman cases keep their
Latin hints.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review: an empty layer gave bounds of 0 x 0, so the canvas took the drag from the Hand tool and
wrote offsets of up to ±4 layer widths from a short drag, for a gradient that paints nothing;
and the drag's one origin stayed when `selected` changed under a held press, so the next instance
moved from the previous origin and the whole travel since the press. `gradient_layer_bounds`
returns `None` for empty bounds, and `__gradientDrag` keeps the instance id, the origin and the
pointer total at that point, taken afresh when the id differs: offsets are the origin plus the
travel since.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review: "šuma" (0x9A u m a) read as Mac Roman "öuma", certain, where main tied it and read
windows-1252; and "¡Hola!" read as Mac Roman "°Hola!". The word-initial rule for windows-1252's
0x80–0x9F letters no longer asks for a capital (the file's own alphabet still decides: a file that
writes ö and never š reads the byte as Mac Roman ö), and ¡ counts as common text for windows-1252
where it opens a word of two letters or more (`opens_word`, in `score` and in the final
windows-1252/Mac Roman comparison), while Mac Roman's degree sign on the same byte, after a
number ("Rotation 90°") or before a single letter ("20 °C"), stays as it was.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review: "ŠKODA" (a capital before an ASCII capital) counted for nothing and could read as Mac
Roman "äKODA"; Mac Roman "Layer 90°é" read as windows-1251 "90ЎЋ", two Cyrillic letters right
after a number. The word-initial rule now takes an ASCII letter of either case after the capital,
and `cyrillic_word` treats a digit before the run as glue (a digit after still isn't: "Слой1").
Main's new fallback test tied Mac Roman "Ändern" with windows-1251, which this branch settles for
Mac Roman; it ties "éäé" with "ЋЉЋ" instead and checks "Ändern" is certain.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Conflict in crates/engine/src/jobs.rs: main's save commit point (#5333,
OPEN/COMMITTED/CANCELLED phase) met the batch's cancel state
(CANCEL_REQUESTED/WORK_FINISHED). Both now share one atomic with a
COMMITTED bit; commit vs cancel and the batch's late-cancel guard each
stay one compare-and-swap. A Batch that refuses a repeated or late cancel
reports CancelRefused::Finishing ("finishing") in jobs.cancel's
notCancelled list, still {"cancelled": 0}. control-protocol.md documents both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A channel or merged image still in its source (an imported file's, or one
spilled to scratch) is copied to the output in 8 MiB pieces instead of being
read whole into memory first.
Resolve against main's save restructure (Services::save_file/SaveCtl, scratch-integrity checks,
ZipWriter::with_entries, exact encoded_len):
- PSD/PSB saves stream the file model through atomic_write_stream_progress; the length used for
progress and reservations is main's exact encoded_len (the PR's size_bound under-counted lazy
data); export_with/encode_with share checked_stages, so main's scratch checks (previews
included) still refuse unreadable scratch.
- Drop ZipWriter::for_entries in favour of main's with_entries.
- TIFF: take_from_psd moves layer data out of the export's own model (never the document);
body() reserves its exact length, failures are OutOfMemory (not LimitExceeded, which would
trigger the PSB retry).
- Streaming writer: AlreadyExists from the writer is a failed save, not a name collision.
- Remove dead Vec put_widened_rle; channel_data_len is test-only.
A rare windows-1252 capital starting a word counts unless the file's Unicode text writes the Mac Roman letter on that byte and never this one (état, über); Слой1 reads as a Cyrillic word. Tests.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A Cyrillic step name in a Latin-hinted set (Ёлка in a French set), a Cyrillic word beside a Latin one (Layer Слой) and a windows-1252 name starting with a capital Š or Ž (Škoda) all read as before; the Mac Roman cases of #10774 still read as Mac Roman. Tests for each.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
With our fix-up 1f524c378b on top of the author's head 713aa92ba5. Resolved with main: the version check sits in main's parse_layout (it merged cleanly); doc comments combined; atn_contract no longer expects version 12 to be refused and pins that an empty version 12 set parses; one ATTRIBUTION row for all ATN fixtures; the fixture README states which files were encoded how; development.md no longer counts the fixtures.
The three version 12 fixtures were tracked inside crates/psd/fuzz/corpus,
which crates/psd/fuzz/.gitignore ignores and cargo-fuzz rewrites (cmin) or
deletes; the psd and engine test binaries include_bytes! them, so cleaning
the corpus broke the build. They move to crates/psd/tests/fixtures/atn with
a README (sizes, purpose, sha256; the seeds note folded in) and an
ATTRIBUTION.md row; the fuzzer takes the folder as an extra corpus dir.
photocraft-cli recognised an extensionless Actions file only by version
word 16, so a version 12 set without .atn (classic Mac OS Photoshop 5)
went down the JSON path and failed with a UTF-8 error. It now accepts 12
and 16, the versions photocraft-psd reads. Input: v12-commands.atn copied
to "Legacy Actions" and passed to load_actions_file.
Co-authored-by: yoriswind <164435053+yoriswind@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Port the ten backend, automation and test paths from #5493 by
@jonathanwxh-cell, pinned at 43920ab780.
Keep its original CSH codec, bounded preset reader and protection tests.
The prerequisite's UI, translation and parity-document changes are excluded.
Adapt ShapePreset construction to retain the exact source identifier. Empty
identifiers remain anonymous presets; repeated identifiers resolve when their
geometry is identical and refuse resolution when it conflicts. Add one
import-to-ATN Shape Blur regression and document the source attribution.
Original contribution: https://github.com/storytold/photocraft/pull/5493
Source-only integration; compilation and combined runtime validation remain.
- #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)