Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46eb1083ca | ||
|
|
639886357d | ||
|
|
d92266a402 |
@@ -481,7 +481,11 @@ fn bitmap_extent(hbm: HBITMAP) -> Option<(u32, u32)> {
|
||||
|
||||
/// Convert the ICONINFO bitmaps to straight RGBA. Two families:
|
||||
/// - color (`hbmColor` set): 32bpp BGRA; if the alpha channel is entirely empty (old-style
|
||||
/// cursors) the AND mask supplies it (mask bit 1 = transparent).
|
||||
/// masked-color cursors, including Windows 11's coloured I-beam) the AND mask is NOT just
|
||||
/// transparency — it is the same four-state table as monochrome, with the colour bitmap
|
||||
/// standing in for the XOR plane. Treating AND=1 as "always transparent" drops invert
|
||||
/// pixels, and the text I-beam is almost entirely invert, so the client would install a
|
||||
/// fully-transparent pointer over every text field.
|
||||
/// - monochrome (`hbmColor` null): `hbmMask` is DOUBLE height — AND plane over XOR plane, the
|
||||
/// WebRTC truth table: (0,0) black, (0,1) white, (1,0) transparent, (1,1) invert. Invert
|
||||
/// pixels — unrepresentable in straight alpha — become opaque black with a white outline
|
||||
@@ -497,12 +501,12 @@ fn convert(ii: &ICONINFO) -> Option<(Vec<u8>, u32, u32)> {
|
||||
let (w, h) = (color.w as u32, color.h as u32);
|
||||
let mut rgba = bgra_to_rgba(&color.bgra);
|
||||
if alpha_is_empty(&rgba) {
|
||||
// Alpha-less color cursor: transparency lives in the AND mask.
|
||||
// Alpha-less color cursor: AND + colour-as-XOR, including invert.
|
||||
let mask = read_bitmap_32(dc, ii.hbmMask)?;
|
||||
if mask.w != color.w || mask.h < color.h {
|
||||
return None;
|
||||
}
|
||||
apply_and_mask_alpha(&mut rgba, &mask.bgra);
|
||||
rgba = masked_color_to_rgba(&rgba, &mask.bgra, w as usize, h as usize);
|
||||
}
|
||||
Some((rgba, w, h))
|
||||
} else {
|
||||
@@ -596,18 +600,44 @@ fn bgra_to_rgba(bgra: &[u8]) -> Vec<u8> {
|
||||
}
|
||||
|
||||
/// Whether a 32bpp RGBA buffer's alpha channel is entirely zero — the "old-style cursor with no
|
||||
/// alpha" test, whose transparency lives in the AND mask instead ([`apply_and_mask_alpha`]).
|
||||
/// alpha" test, whose transparency (and invert) live in the AND mask instead
|
||||
/// ([`masked_color_to_rgba`]).
|
||||
fn alpha_is_empty(rgba: &[u8]) -> bool {
|
||||
rgba.chunks_exact(4).all(|p| p[3] == 0)
|
||||
}
|
||||
|
||||
/// Take alpha from an expanded AND mask: mask WHITE (AND bit 1) means transparent, black opaque.
|
||||
/// `mask_bgra` is the 32bpp expansion `GetDIBits` produces from the 1bpp mask, so any non-zero
|
||||
/// channel byte is "set".
|
||||
fn apply_and_mask_alpha(rgba: &mut [u8], mask_bgra: &[u8]) {
|
||||
for (px, m) in rgba.chunks_exact_mut(4).zip(mask_bgra.chunks_exact(4)) {
|
||||
px[3] = if m[0] != 0 { 0 } else { 0xFF };
|
||||
/// Alpha-less colour cursor: the AND mask plus the colour bitmap as XOR, same four states as
|
||||
/// [`mono_planes_to_rgba`]. A non-zero RGB with AND=1 is invert — which treating AND=1 as
|
||||
/// "always transparent" would have dropped, vanishing the I-beam.
|
||||
///
|
||||
/// `(false, true)` keeps the colour (a painted glyph), unlike the monochrome table which can
|
||||
/// only emit white.
|
||||
fn masked_color_to_rgba(color_rgba: &[u8], mask_bgra: &[u8], w: usize, h: usize) -> Vec<u8> {
|
||||
let mut rgba = vec![0u8; w * h * 4];
|
||||
let mut invert = vec![false; w * h];
|
||||
for i in 0..w * h {
|
||||
// Either source running short leaves the pixel TRANSPARENT — the quiet failure. Reading a
|
||||
// missing mask byte as AND=0 instead would land in the opaque arms and paint a black box
|
||||
// over the pointer, which is the loudest way to be wrong. `convert` already requires the
|
||||
// mask to cover the colour bitmap, so this is only the belt.
|
||||
let (Some(&m), Some(c)) = (mask_bgra.get(i * 4), color_rgba.get(i * 4..i * 4 + 3)) else {
|
||||
continue;
|
||||
};
|
||||
let and = m != 0;
|
||||
let xor = c[0] != 0 || c[1] != 0 || c[2] != 0;
|
||||
let px = &mut rgba[i * 4..i * 4 + 4];
|
||||
match (and, xor) {
|
||||
(false, false) => px.copy_from_slice(&[0, 0, 0, 0xFF]),
|
||||
(false, true) => px.copy_from_slice(&[c[0], c[1], c[2], 0xFF]),
|
||||
(true, false) => {} // transparent (already zeroed)
|
||||
(true, true) => {
|
||||
px.copy_from_slice(&[0, 0, 0, 0xFF]);
|
||||
invert[i] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
grow_invert_outline(&mut rgba, &invert, w, h);
|
||||
rgba
|
||||
}
|
||||
|
||||
/// The monochrome-cursor truth table, plus the white outline that makes an INVERT region legible.
|
||||
@@ -645,6 +675,13 @@ fn mono_planes_to_rgba(and_plane: &[u8], xor_plane: &[u8], w: usize, h: usize) -
|
||||
}
|
||||
}
|
||||
}
|
||||
grow_invert_outline(&mut rgba, &invert, w, h);
|
||||
rgba
|
||||
}
|
||||
|
||||
/// Turn every TRANSPARENT 8-neighbour of an invert pixel opaque white. Invert itself stays
|
||||
/// opaque black. Shared by the monochrome and masked-color converters.
|
||||
fn grow_invert_outline(rgba: &mut [u8], invert: &[bool], w: usize, h: usize) {
|
||||
for y in 0..h as i32 {
|
||||
for x in 0..w as i32 {
|
||||
if !invert[(y * w as i32 + x) as usize] {
|
||||
@@ -662,7 +699,6 @@ fn mono_planes_to_rgba(and_plane: &[u8], xor_plane: &[u8], w: usize, h: usize) -
|
||||
}
|
||||
}
|
||||
}
|
||||
rgba
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -771,27 +807,70 @@ mod tests {
|
||||
assert!(alpha_is_empty(&[]), "no pixels ⇒ vacuously empty");
|
||||
}
|
||||
|
||||
/// Mask WHITE (AND bit 1) = transparent, black = opaque — and the colour bytes are untouched.
|
||||
// ---- masked-color (colour bitmap as XOR) ------------------------------------------------
|
||||
|
||||
/// The four-state table, with colour standing in for XOR. Pixel 3 is the I-beam case: AND=1
|
||||
/// and a non-zero colour pixel is invert, not transparent — the old AND-as-alpha helper read
|
||||
/// AND=1 as "always transparent" and dropped it, which is how the text cursor vanished on a
|
||||
/// Windows host.
|
||||
#[test]
|
||||
fn the_and_mask_supplies_alpha_for_an_alpha_less_cursor() {
|
||||
let mut rgba = vec![
|
||||
10, 20, 30, 0, // pixel 0
|
||||
40, 50, 60, 0, // pixel 1
|
||||
fn a_masked_color_invert_pixel_is_not_transparent() {
|
||||
// (0,0) black (0,1) red (1,0) transparent (1,1) invert
|
||||
let color = vec![
|
||||
0, 0, 0, 0, //
|
||||
0xCC, 0, 0, 0, //
|
||||
0, 0, 0, 0, //
|
||||
0xFF, 0xFF, 0xFF, 0, //
|
||||
];
|
||||
let mask = plane(&[1, 0]); // pixel 0 masked out, pixel 1 kept
|
||||
apply_and_mask_alpha(&mut rgba, &mask);
|
||||
assert_eq!(px(&rgba, 0), [10, 20, 30, 0], "masked ⇒ transparent");
|
||||
assert_eq!(px(&rgba, 1), [40, 50, 60, 0xFF], "unmasked ⇒ opaque");
|
||||
let mask = plane(&[0, 0, 1, 1]);
|
||||
let out = masked_color_to_rgba(&color, &mask, 4, 1);
|
||||
assert_eq!(px(&out, 0), OPAQUE_BLACK, "AND=0 colour=0 ⇒ black");
|
||||
assert_eq!(
|
||||
px(&out, 1),
|
||||
[0xCC, 0, 0, 0xFF],
|
||||
"AND=0 colour ⇒ opaque colour"
|
||||
);
|
||||
// Pixel 2 is transparent by the table, but it is an 8-neighbour of the invert pixel at 3,
|
||||
// so the outline claims it — same as the monochrome table.
|
||||
assert_eq!(
|
||||
px(&out, 2),
|
||||
OPAQUE_WHITE,
|
||||
"outline grows into adjacent transparency"
|
||||
);
|
||||
assert_eq!(
|
||||
px(&out, 3),
|
||||
OPAQUE_BLACK,
|
||||
"AND=1 colour≠0 ⇒ invert, not drop"
|
||||
);
|
||||
}
|
||||
|
||||
/// A mask with FEWER pixels than the colour bitmap must not panic — `zip` stops at the shorter
|
||||
/// side, leaving the tail at whatever alpha it had (the caller has already required
|
||||
/// `mask.h >= color.h`, so this is the belt).
|
||||
/// AND=1 and a zero colour pixel stays transparent when nothing invert-neighbours it.
|
||||
#[test]
|
||||
fn a_short_mask_does_not_panic() {
|
||||
let mut rgba = vec![1, 2, 3, 0, 4, 5, 6, 0, 7, 8, 9, 0];
|
||||
apply_and_mask_alpha(&mut rgba, &plane(&[0]));
|
||||
assert_eq!(px(&rgba, 0), [1, 2, 3, 0xFF]);
|
||||
assert_eq!(px(&rgba, 1), [4, 5, 6, 0]);
|
||||
fn a_masked_color_transparent_pixel_stays_transparent() {
|
||||
let color = vec![0u8; 16];
|
||||
let mask = plane(&[1, 1, 1, 1]);
|
||||
let out = masked_color_to_rgba(&color, &mask, 4, 1);
|
||||
for i in 0..4 {
|
||||
assert_eq!(px(&out, i), TRANSPARENT, "pixel {i}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Either source running short must not panic AND must leave the uncovered tail transparent —
|
||||
/// reading a missing byte as a zero would land in the opaque arms and paint a black box over
|
||||
/// the pointer. `convert` already requires `mask.h >= color.h`, so this is only the belt.
|
||||
#[test]
|
||||
fn a_masked_color_short_source_leaves_the_tail_transparent() {
|
||||
let color = vec![0xFF; 16]; // four white pixels...
|
||||
let out = masked_color_to_rgba(&color, &plane(&[0]), 4, 1); // ...but one mask pixel
|
||||
assert_eq!(px(&out, 0), OPAQUE_WHITE, "the covered pixel converts");
|
||||
for i in 1..4 {
|
||||
assert_eq!(px(&out, i), TRANSPARENT, "mask ran short at pixel {i}");
|
||||
}
|
||||
|
||||
let out = masked_color_to_rgba(&color[..4], &plane(&[0, 0, 0, 0]), 4, 1);
|
||||
assert_eq!(px(&out, 0), OPAQUE_WHITE, "the covered pixel converts");
|
||||
for i in 1..4 {
|
||||
assert_eq!(px(&out, i), TRANSPARENT, "colour ran short at pixel {i}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user