Every open path read the whole file with std::fs::read, which asks
Windows for up to 4 GiB in a single ReadFile. Some file systems (exFAT,
network redirectors) reject a read that large with
ERROR_INVALID_PARAMETER, "The parameter is incorrect" (os error 87),
as reported for a 4.75 GB PSB in #375.
Add photocraft_format::read_file and read_all beside atomic_write:
64 MiB reads into a buffer reserved up front, and an OutOfMemory error
that says how much the file needed. Route every document read through
them:
- the background open job (engine jobs.rs read_path);
- the engine's read_file;
- Open Recent and OS open events;
- desktop drag and drop, which now goes through open_path (and so
through the background job when jobs are on);
- the File > Open dialog;
- .pcraft loading;
- the automation workspace read and automation::files::open;
- smart object Replace Contents and linked files.
The dialog no longer fails silently: PickOpenFn returns the picked name
with the read result, and a failure is reported through open_failed and
the notices like any other open failure.
Speed is unchanged (1.11 s vs 1.13 s for a warm 4.75 GB file).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Smart objects keep their four-corner (Distort / Perspective) placement
A smart object's placement was an affine transform, so a perspective
placement lost its fourth corner: a mockup PSD's distorted screen looked
right on open (Photoshop's cached pixels) but re-rendered as a
parallelogram after Edit Contents → Save.
- SmartObject gains `perspective`, the full projective map (source px →
document px); `transform` keeps its affine approximation
- Re-renders go through it (place_source_projective), as do moves,
canvas rotations and edit.transform, so Distort / Perspective now work
on smart objects instead of being refused
- PSD import reads the quad (nonAffineTransform / Trnf) and export writes
it back; .pcraft stores the field (absent for affine placements)
- Test: a distorted smart object survives Edit Contents → Save
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Smart objects: warp maps through a distorted placement; perspective round-trip tests
Warp handles and stored warps on a Distort / Perspective smart object go through its full
projective placement (smart_cmds::placement) instead of the affine approximation, so they sit
on the distorted corners. Tests: SoLd perspective round trip and quad_moved (io), a perspective
smart layer in the .pcraft rich_doc, warp on a distorted smart object (engine). Comments no
longer name another product.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Jason Short <3344856+jason4short@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* PSD: reject a header with zero width or height (#519)
A 0xN header opened as an empty document and saved back as another
zero-sized PSD or .pcraft. Header::validate now requires at least 1x1
(the spec range starts at 1), and PSD export validates the header so a
zero-sized canvas is never written.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* .pcraft: documents with deeply nested groups load again (#517)
serde_json's 128-level recursion limit made bundles with 40 or more
nested groups unreadable after a successful save. The manifest is now
parsed with that limit disabled behind a linear JSON depth check, layers
are decoded and converted one level at a time with small stack frames,
and groups may nest up to MAX_GROUP_DEPTH (100) levels, which load and
save within a few hundred KiB of stack. Saving a deeper document fails
instead of writing a bundle that can't be opened.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Export: convert non-sRGB RGB to sRGB for formats without a profile (#522)
GIF, BMP, QOI, TGA, ICO and Netpbm can't embed ICC, so a linear-light
or Display P3 document was written with values that only meant something
under the dropped profile (a linear EXR came out dark). export_flat now
converts such RGB to sRGB through photocraft-cms (perceptual, as Quick
Export does) for every photocraft_io::export route. The EXR/HDR linear
conversion shares the same banded helper.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Export: composite transparency over white for formats without alpha (#521)
The CLI, MCP, Save As, Layers to Files and Layer > Export As dropped the
alpha channel when writing JPEG (or Radiance HDR), exposing the colours
stored under transparent pixels, while Export As and Quick Export
composite over white. export_flat now flattens over white too (no ink
for CMYK) and says so in its warnings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Launch no longer deletes recovery entries right after loading them. A
recovered document adopts the entry it came from: its next autosave
replaces it in place (atomically) and saving or closing the document
removes it, so a second crash before the next autosave loses nothing.
New documents autosave under per-launch keys, so a document id reused
from an earlier launch never overwrites a surviving entry, and sidecars
whose key doesn't match their file name are ignored.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- codecs: bump tiff 0.10 -> 0.11. tiff 0.10 indexed an empty SampleFormat
list and panicked on a crafted TIFF (found by mutation fuzzing);
regression test tiff_empty_sample_format_is_an_error.
- codecs: Image::new returns Result instead of panicking on size overflow.
- format: par_compress returns an error if a compression worker panics.
- io: par_map joins every worker before relaying a worker panic, so the
thread scope can't panic on an unjoined thread.
- All four crates now deny unwrap/expect/panic/unimplemented/todo/
unreachable outside tests.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Rust 1.99's clippy adds chunks_exact_to_as_chunks and some_filter, which
broke the Clippy step on every CI runner (it uses the latest stable). All
sites are mechanical: as_chunks drops the remainder exactly like
chunks_exact.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI's `cargo fmt --all -- --check` has failed on every run because the
repo had no rustfmt config and was never formatted. Use the same settings
as EffectCraft/FilmCraft (max_width 160, use_small_heuristics Max), which
also keeps the churn small. Formatting only; no code changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model Blend If (This Layer / Underlying Layer ranges, split points) as
Layer::blend_if, read and write it as the PSD layer-record blending
ranges (previously parsed but dropped on export), save it in .pcraft,
render it in the CPU compositor (premultiplied mix = compositing at
k × alpha), fall back to the CPU on the GPU path, and drive it from
layer.layerStyle.blendingOptions {"blendIf": …}; document.inspect
reports it.
Co-authored-by: storybored379 <storybored379@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
VideoDataM stores each frame via the surface blob store (surface_m /
self.surface), with source/fps/show_altered; threaded both directions in
layer_m and the loader. A saved video layer reloads its full frame stack,
timeline and footage source. Round-trip test covers pixels + metadata.
format 16 tests, clippy + wasm green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Layer.video (VideoData: frame surfaces + fps + source); the layer's
displayed content is the frame at the timeline playhead, re-synced after
any edit/navigation. Commands: New Blank Video Layer, Insert Blank /
Duplicate / Delete / Restore Frame, Restore All Frames, Show Altered
Video, Rasterize (Layer > Video Layers > Rasterize and Rasterize > Video).
Timeline duration grows to fit. 4 tests; engine green, clippy clean, wasm
OK. Parity 609 -> 618 (98.9%); FLOOR 613. (File-based footage + Render
Video + Video Frames to Layers + .pcraft frame persistence are follow-ups.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Document.timeline (fps/duration/playhead/work-area), threaded through
.pcraft. Engine timeline.{create,delete,setFrame,next/previousFrame,
setProps,info} (navigation is non-history, scriptable for frame-range
rendering); reported in document.inspect. Foundation for Layer > Video
Layers + the Timeline panel (follow-up arcs). doc 4 + engine 2 tests;
format/doc/engine green, clippy clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Clean-room implementation of Photoshop's data-driven graphics, headless
and scriptable for CLI/MCP batch generation:
- doc: Variables model (visibility/text/pixel-replacement bindings + named
data sets), threaded through .pcraft (manifest + convert).
- engine variables_cmds.rs: image.variables.define, image.variables.dataSets,
image.applyDataSet (visibility/text/pixel replace w/ fit/fill/conform/as-is
+ 9 alignments, one history step), file.import.variableDataSets (CSV),
file.export.dataSetsAsFiles (apply each set and export), variables.list.
- Pixel replacement loads+scales+aligns an image into the layer bounds via
the existing resample helpers.
- 7 tests (doc round-trip + lookups; engine define/apply, CSV import, export,
pixel replace, bad params). Parity 600 -> 605; FLOOR 600.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>