* perf(display): encode linear documents for the screen in parallel
CanvasDisplay::texture_buffer (every CPU-path canvas upload of a linear
document, which every 32-bit document is since #3792) and to_rgba8's sRGB
encode ran one pixel at a time on one thread. Both now split the pixels into
16K-pixel pieces across rayon off wasm; results are bit-identical.
24 MP linear document, median of 5 (22-thread laptop, dev profile):
- texture_buffer: 348.5 ms -> 29.6 ms
- to_rgba8: 107.8 ms -> 10.6 ms
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbyXia7KGdFgbNjBeggY8J
* perf(compose): quantize Buffer::to_rgba8 in parallel
The plain 8-bit quantize behind the CPU canvas, previews, thumbnails and
8-bit exports ran on one thread. Split it into 16K-pixel pieces across
rayon off wasm; results are bit-identical.
24 MP buffer, median of 5 (22-thread laptop, dev profile): 122.3 ms -> 11.9 ms
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbyXia7KGdFgbNjBeggY8J
* perf(canvas): build canvas images in parallel, fused with the quantize
display_image (CPU canvas updates, filter previews, the navigator, the crop
pasteboard) quantized to RGBA8 and then ran egui's single-threaded
ColorImage::from_rgba_unmultiplied over every pixel. The identity path now
quantizes straight to Color32 in one parallel pass; the colour-managed path
converts in parallel. Results are identical to egui's conversion.
24 MP buffer, median of 5 (22-thread laptop, dev profile): 50.9 ms -> 12.7 ms
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbyXia7KGdFgbNjBeggY8J
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
arboard 3.6 asks the clipboard for image/png only, so a screenshot tool or
clipboard manager that offers just image/jpeg, image/bmp or image/webp pasted
nothing ("the clipboard holds no image"). When arboard finds no image, read
the other image/* types through wl-clipboard-rs (already in Cargo.lock via
arboard's wayland-data-control) and decode them with photocraft-codecs.
Reads are capped at 1 GiB; TGA is trusted only when its type says so.
Claude-Session: https://claude.ai/code/session_01AbyXia7KGdFgbNjBeggY8J
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* perf(camera-raw): convert the refined preview in parallel bands, not per row (#4682)
#3427 colour-managed the refined (zoomed-in) Camera Raw preview by calling
the display transform once per row. A row is below photocraft-cms's 16K-pixel
parallel threshold, so every edit ran the whole conversion on one core of
the UI thread, plus two allocations per row. Read and convert 64K-pixel bands
instead, in parallel off wasm (as channel_view does); output is identical.
2560x1600 visible crop, median of 15 runs (22-thread laptop, dev profile):
- sRGB document: 31.5 ms -> 3.9 ms
- Adobe RGB document: 125.0 ms -> 12.5 ms
- ProPhoto document: 117.5 ms -> 12.7 ms
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbyXia7KGdFgbNjBeggY8J
* perf(camera-raw): the main preview converts in parallel bands too, without a full copy
CameraRawDialog::image copied the whole proxy into a Buffer and colour-converted
it on one thread on every edit. It now goes through the same banded, parallel
display_image as the refined preview, reading bands straight from the slice.
Median of 21 runs (22-thread laptop, dev profile):
- 858x572 sRGB: 4.72 ms -> 1.00 ms
- 858x572 Adobe RGB: 7.39 ms -> 2.06 ms
- 1716x1144 sRGB: 18.58 ms -> 2.48 ms
- 1716x1144 Adobe RGB: 28.78 ms -> 6.88 ms
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbyXia7KGdFgbNjBeggY8J
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
When drawing faux-bold glyphs, the offset dilation was composed as
`Xform(ox * r, oy * r).mul(&glyph)`. Because `Xform::mul(o)` applies `o`
first (`self ∘ o`), the translation offset was added after the glyph's
scaling transform, leaving faux bold dilation unscaled by horizontal
and vertical font scaling.
Applying the glyph transform to the offset transform (`glyph.mul(&offset)`)
ensures dilation is performed in font-local coordinates and scaled
proportionally with anisotropic glyph scaling, matching Photoshop.