* Compose: render in place and in bands; area-reduced and proxy thumbnails
render_tiled wrote every tile into a Vec and then copied them into the output:
two full-size float copies (6.3 GB for a 14000² document). Tiles are now
written straight into the output band by band.
New streaming APIs so large documents never need a full-size float
composite: render_bands (sink per band of ~8 MP), render_reduced (banded
area average, bit-identical to the old flatten-then-average thumbnail) and
flatten_to_surface (Flatten Image, Merge Visible, Copy Merged and the other
flattened-surface users).
thumbnail() reduces a downsampled proxy (moved from ui-egui to
compose::proxy, now also scaling spot channels) for documents over 16 MP
when that is faithful (no layer effects or vector masks), so navigator,
channel and histogram thumbnails of a 196 MP document cost milliseconds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* GPU canvas: request the adapter's real limits; banded CPU fallback; navigator thumbnail
egui-wgpu asks for an 8192 px texture limit, so the wgpu compositor rejected
every document wider or taller than 8192 px and each refresh fell back to a
full CPU flatten. The desktop and web apps now request the adapter's own
max_texture_dimension_2d and max_buffer_size (gpu_canvas::use_adapter_limits);
the benches use the same limits.
The CPU fallback (documents beyond the GPU limit, or features it lacks) is
GpuCanvas::refresh: it renders and uploads the composite band by band
instead of a full float composite plus a full RGBA8 copy. The CPU canvas
texture is reduced in bands too, and the Navigator on the GPU canvas uses a
cached thumbnail instead of a full-resolution CPU composite per revision.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* IO: banded export and import; no full-size copies in encoders; larger decode limits
- Flat export renders the composite in bands and quantises each band; alpha
is dropped in place when every pixel is opaque. The single-layer native
path copies surface rows a band at a time (no f32 read_region and no
per-pixel Vec). Buffers come from try_reserve: Err, not an abort.
- PSD export encodes the merged image band by band into its planes (no
flatten, no second plane copy); near-opaque documents are re-encoded
without the white matte so the output is unchanged. Layer interleaved
bytes are freed before their planes are compressed.
- Flat import converts in ~32 MB bands (and not at all when already RGBA);
the PSD merged-image un-matte works per tile row.
- Encoders borrow the image when no conversion is needed (Image::converted)
instead of cloning it; PNG only copies 16-bit data for the byte swap.
- Decode limits: 2^30 pixels and 8 GiB on 64-bit targets (a 20000² JPEG
was refused at 2^28).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Filters: write tile results in memory-bounded groups
apply_tiled collected every tile's float result before writing any (3.1 GB
for a 14000² layer). Results are now produced and written in groups of about
256 MB (at least one tile per core).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* History: bound pixel memory by Preferences > Performance > Memory Usage
History::max_bytes caps the current document's tiles plus the tiles only
history holds (layers, masks, alpha channels); History::trim drops the
oldest states beyond it after every recorded step and when preferences
change, always keeping the last step undoable. The engine sets it from
memory_usage_mb, which was unused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Large-image benchmark (14k / 20k): open, refresh, thumbnails, filter, stroke, saves
One operation per run on a cached synthetic photo, with the GPU device created
the way the app creates it, so /usr/bin/time -l reports that operation's peak
memory. Documented in docs/development.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* raw: clean-room camera raw decoder and developer (photocraft-raw)
A new standalone crate (no workspace deps, wasm-safe, no unsafe) written
from TIFF 6.0, TIFF/EP, the DNG 1.7 spec, ITU-T T.81 Annex H and published
demosaicing papers:
- Lossless JPEG (LJ92) decoder: predictors 1-7, point transform, restart
intervals, combined Huffman+extra-bits fast table.
- DNG: uncompressed (8-16 bit, packed) and LJ92, strips and tiles, CFA and
LinearRaw, linearization, black level (repeat, deltas), white level,
active area, default crop, ColorMatrix/CameraCalibration/ForwardMatrix
interpolation by CCT, AnalogBalance, AsShotNeutral/WhiteXY,
BaselineExposure, orientation.
- CR2 (LJ92 slices, SensorInfo borders, ColorData as-shot WB, measured
black) and TIFF/EP raws (NEF/ARW/PEF uncompressed or LJ92, incl. Sony
lossless ARW tiles, Nikon/Sony documented black and WB tags).
- Develop: black/white scaling, WB with highlight clipping, bilinear /
Malvar-He-Cutler / AHD demosaic (rayon bands, sequential on wasm),
camera -> XYZ D50 -> ProPhoto, exposure, gamma 1.8, 16-bit, orientation.
- Embedded JPEG preview finder; clear Unsupported errors for CR3, RAF,
ORF, RW2, vendor-compressed NEF/ARW/PEF, lossy/JXL/float DNG, X-Trans.
- Limits checked before every allocation; testgen feature with an LJ92
encoder and DNG/CR2/TIFF-EP writers; round-trip, colour-reference,
truncation and corruption tests.
Registered as a standalone crate in xtask layers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* io: open camera raws through photocraft-raw (16-bit ProPhoto), preview fallback
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* raw: DNG GainMap opcode, fuzz targets, stress tools; open raws from the apps
- Apply DNG OpcodeList2 GainMap (lens shading, e.g. phone DNGs); report other opcodes.
- cargo-fuzz targets (develop, ljpeg) and a mutation stress example.
- Raw extensions in the desktop/web open dialogs and the batch folder list.
- file.open returns the import notes (how a raw was developed, preview fallback).
- codecs: the camera raw refusal points at photocraft-raw.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* raw: clippy 1.99 (is_multiple_of, clamp)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
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>
Camera raw files are TIFF-structured, so they reached the TIFF decoder.
A raw image with the TIFF/EP CFA or DNG LinearRaw photometric
interpretation failed with "Tiff: unknown photometric interpretation",
a CR2 with "colour type YCbCr(8)", and a DNG or a raw whose sensor data
sits in a SubIFD behind an RGB IFD0 quietly opened that small preview
as if it were the photo.
The TIFF decoder now checks the structure first (CR2 signature,
DNGVersion tag, CFA / LinearRaw image in IFD0, its SubIFDs or the next
IFDs; bounds-checked header reads only) and returns Unsupported:
"camera raw files (such as CR2, NEF, ARW or DNG) are not supported".
Ordinary TIFFs decode as before. Tests use tiny synthetic TIFFs with
those markers, including truncations.
Co-authored-by: storybored379 <storybored379@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
codecs: encode_gif_animated (GIF89a multi-frame, per-frame local palettes,
NETSCAPE loop, delay centiseconds) + GifFrame; 2 tests. video render_video
gains a gif path: composite the document at each timeline frame, quantise
(adaptive 256), and write one looping .gif; image-sequence path unchanged.
engine 7 video tests, clippy + wasm green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Gradients/Patterns/Styles/Shapes/Tool Presets/Clone Source panels with
headless preset commands and a Custom Shape tool; Color card tabs match
Essentials. Character and Paragraph Styles (doc model, .pcraft, PSD style
sheets), Glyphs panel + type.insertText, Check Spelling (SCOWL-50 list).
Checkpoint also includes compiling in-progress work of the remaining agents.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>