Layer via Copy, Copy and Cut read only the selected area: 33x faster on 24 MP (#668) (#985)

lift() converted the whole layer to a format with alpha before reading the
selection's rectangle, so on a 24 MP Background every Layer via Copy cost about
a second whatever the selection's size, and it sampled the selection mask one
pixel at a time. It now reads just that rectangle and the mask's rectangle.

6000x4000 Background, 2000x1500 selection, release: Layer via Copy 984 ms ->
30 ms. Select All then Layer via Copy: 212 ms.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas
2026-10-08 03:17:48 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 1ae11a4bb1
commit d64f809e4d
+47 -5
View File
@@ -68,13 +68,26 @@ fn lift(src: &Surface, sel: Option<&Surface>, canvas: Rect) -> Clip {
if area.is_empty() {
return Clip { surface: out, bounds: Rect::EMPTY };
}
let conv = src.convert(with_alpha);
let mut px = conv.read_region(area);
// Only the selected area is read: converting the whole layer (a 24 MP Background gaining
// alpha) cost about a second per Layer via Copy (#668).
let n = with_alpha.channels();
let mut px = if fmt.alpha {
src.read_region(area)
} else {
let opaque = src.read_region(area);
let k = fmt.channels();
let mut v = Vec::with_capacity(opaque.len() / k.max(1) * n);
for p in opaque.chunks_exact(k.max(1)) {
v.extend_from_slice(p);
v.push(1.0);
}
v
};
if let Some(m) = sel {
let w = area.width() as usize;
for (i, p) in px.chunks_exact_mut(n).enumerate() {
p[n - 1] *= m.sample_channel(area.x0 + (i % w) as i32, area.y0 + (i / w) as i32, 0);
let mask = m.read_region(area);
let mk = m.format().channels().max(1);
for (p, a) in px.chunks_exact_mut(n).zip(mask.chunks_exact(mk)) {
p[n - 1] *= a[0];
}
}
out.write_region(area, &px);
@@ -695,6 +708,35 @@ mod tests {
assert_eq!(px(&s, 0, 8, 8)[3], 0.0);
}
#[test]
fn layer_via_copy_of_a_background_keeps_colour_and_partial_selection() {
// #668: lift reads only the selected area now; the result must be what converting the
// whole layer gave: the colour, with the selection's coverage as alpha.
for depth in [8, 16, 32] {
let mut s = Session::new();
s.execute("file.new", json!({"width": 40, "height": 30, "depth": depth, "background": "#336699"})).unwrap();
s.edit("partial selection", |doc, _| {
let mut m = photocraft_raster::Surface::new(PixelFormat::GRAY8);
m.fill_rect(Rect::new(5, 5, 15, 15), &[1.0]);
m.fill_rect(Rect::new(15, 5, 20, 15), &[0.5]);
doc.selection = Some(m);
Ok(())
})
.unwrap();
let id = s.execute("layer.new.layerViaCopy", json!({})).unwrap()["layer"].as_u64().unwrap();
let d = s.active().unwrap();
let surf = d.doc.layer(LayerId(id)).unwrap().surface().unwrap();
assert!(surf.format().alpha, "@{depth}");
let full = surf.read_region(Rect::new(6, 6, 7, 7));
let half = surf.read_region(Rect::new(16, 6, 17, 7));
for (got, want) in full.iter().zip([0.2, 0.4, 0.6, 1.0]) {
assert!((got - want).abs() < 0.01, "@{depth}: {full:?}");
}
assert!((half[3] - 0.5).abs() < 0.01 && (half[0] - 0.2).abs() < 0.01, "@{depth}: {half:?}");
assert_eq!(surf.content_bounds(), Rect::new(5, 5, 20, 15), "@{depth}");
}
}
#[test]
fn cut_clears_and_layer_via_copy_cut() {
let mut s = session();