From 7c6102c12f4abcf6c95a98c42d31289c404afcf0 Mon Sep 17 00:00:00 2001 From: Himanshu Kumar <77563702+himanshu748@users.noreply.github.com> Date: Sun, 25 Oct 2026 12:56:26 +0000 Subject: [PATCH] fix(desktop-gpui): preserve translucent pixels in mask-only exports --- apps/desktop-gpui/src/screenshot_export.rs | 139 +++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/apps/desktop-gpui/src/screenshot_export.rs b/apps/desktop-gpui/src/screenshot_export.rs index 5533f188441..525858d4fea 100644 --- a/apps/desktop-gpui/src/screenshot_export.rs +++ b/apps/desktop-gpui/src/screenshot_export.rs @@ -385,6 +385,12 @@ fn skia_stroke(width: f64, round: bool) -> tiny_skia::Stroke { /// order -- rect, ellipse, arrow, freehand with tiny-skia, text with /// cosmic-text, each under its own `globalAlpha`. fn draw_annotations_onto(canvas: &mut [u8], width: u32, height: u32, scaled: &[Annotation]) { + if scaled + .iter() + .all(|annotation| annotation.annotation_type == AnnotationType::Mask) + { + return; + } pixels::premultiply(canvas); if let Some(mut pixmap) = tiny_skia::PixmapMut::from_bytes(canvas, width, height) { for annotation in scaled { @@ -659,6 +665,79 @@ mod tests { }; } + #[test] + fn mask_only_draw_preserves_straight_alpha_pixels() { + let original: Vec = (0..=255u8).flat_map(|alpha| [17, 30, 77, alpha]).collect(); + for scaled in [ + Vec::new(), + vec![annotation(AnnotationType::Mask, 0., 0., 16., 16.)], + ] { + let mut canvas = original.clone(); + draw_annotations_onto(&mut canvas, 16, 16, &scaled); + assert_eq!(canvas, original); + } + } + + #[test] + fn off_canvas_mask_does_not_change_translucent_export_pixels() { + let raw = RawFrame { + rgba: vec![17, 30, 77, 1, 123, 45, 67, 127], + width: 2, + height: 1, + base_width: 2, + base_height: 1, + }; + let mut config = ProjectConfiguration::default(); + invisible_background(&mut config); + config + .annotations + .push(annotation(AnnotationType::Mask, 3., 0., 2., 1.)); + + let out = composite(&raw, &config); + + assert_eq!((out.width, out.height), (2, 1)); + assert_eq!(out.rgba, raw.rgba); + } + + #[test] + fn pixelated_export_preserves_pixels_outside_the_mask_and_draws_shapes() { + let raw = RawFrame { + rgba: vec![ + 255, 0, 0, 255, 0, 255, 0, 255, 17, 30, 77, 1, 123, 45, 67, 127, 255, 255, 0, 255, + 0, 0, 255, 255, 23, 41, 83, 3, 111, 55, 99, 129, + ], + width: 4, + height: 2, + base_width: 4, + base_height: 2, + }; + let mut config = ProjectConfiguration::default(); + invisible_background(&mut config); + let mut mask = annotation(AnnotationType::Mask, 0., 0., 2., 2.); + mask.mask_type = Some(cap_project::MaskType::Pixelate); + mask.mask_level = Some(2.); + config.annotations.push(mask); + let mut expected = raw.rgba.clone(); + for index in [0, 1, 4, 5] { + expected[index * 4..index * 4 + 4].copy_from_slice(&[0, 0, 255, 255]); + } + + let out = composite(&raw, &config); + + assert_eq!((out.width, out.height), (4, 2)); + assert_eq!(out.rgba, expected); + + let mut rectangle = annotation(AnnotationType::Rectangle, 2., 0., 2., 2.); + rectangle.fill_color = "#ff0000".into(); + rectangle.stroke_width = 0.; + config.annotations.push(rectangle); + for index in [2, 3, 6, 7] { + expected[index * 4..index * 4 + 4].copy_from_slice(&[255, 0, 0, 255]); + } + + assert_eq!(composite(&raw, &config).rgba, expected); + } + /// `stableValue`: keys sorted at every depth, arrays kept in order. #[test] fn stable_stringify_sorts_keys_recursively() { @@ -904,6 +983,66 @@ mod tests { ); } + #[test] + #[ignore] + fn benchmark_mask_only_screenshot_composite() { + use std::{hint::black_box, time::Instant}; + for (width, height) in [(1920, 1080), (3840, 2160)] { + for translucent_border in [false, true] { + let raw = RawFrame { + rgba: (0..width * height) + .flat_map(|i: u32| { + let x = i % width; + let y = i / width; + let border = x < 32 || y < 32 || x >= width - 32 || y >= height - 32; + let alpha = if translucent_border && border { + (i % 254 + 1) as u8 + } else { + 255 + }; + [ + i as u8, + i.wrapping_mul(17) as u8, + i.wrapping_mul(31) as u8, + alpha, + ] + }) + .collect(), + width, + height, + base_width: width, + base_height: height, + }; + let mut config = ProjectConfiguration::default(); + invisible_background(&mut config); + let mut mask = annotation(AnnotationType::Mask, 100., 100., 256., 256.); + mask.mask_type = Some(cap_project::MaskType::Pixelate); + mask.mask_level = Some(16.); + config.annotations.push(mask); + let mut samples_ns = Vec::new(); + for iteration in 0..35 { + let start = Instant::now(); + black_box(composite(black_box(&raw), black_box(&config))); + let elapsed = start.elapsed().as_nanos(); + if iteration >= 5 { + samples_ns.push(elapsed); + } + } + samples_ns.sort_unstable(); + let median_ms = (samples_ns[14] + samples_ns[15]) as f64 / 2_000_000.; + println!( + "{}", + serde_json::json!({ + "width": width, "height": height, + "translucent_border": translucent_border, + "median_ms": median_ms, "samples_ns": samples_ns, + "scope": "CPU mask-only composite; excludes GPU, PNG, disk and native UI" + }) + ); + } + } + } + #[test] #[ignore] fn benchmark_screenshot_copy_preparation() {