@@ -977,8 +977,9 @@ impl AndroidGrpcFrameStream {
977977 grpc:: image_format:: ImgFormat :: try_from ( format. format )
978978 . unwrap_or ( grpc:: image_format:: ImgFormat :: Rgba8888 ) ,
979979 ) ?;
980- let ( width, height, rgba) =
980+ let ( width, height, mut rgba) =
981981 normalize_android_frame_orientation ( width, height, rgba, self . target ) ;
982+ flatten_android_frame_alpha ( & mut rgba, width, height) ;
982983 Ok ( Some ( AndroidFrame {
983984 width,
984985 height,
@@ -1041,6 +1042,59 @@ fn rotate_rgba_180_in_place(rgba: &mut [u8], width: u32, height: u32) {
10411042 }
10421043}
10431044
1045+ fn flatten_android_frame_alpha ( rgba : & mut [ u8 ] , width : u32 , height : u32 ) {
1046+ if !rgba. chunks_exact ( 4 ) . any ( |pixel| pixel[ 3 ] != 255 ) {
1047+ return ;
1048+ }
1049+
1050+ let width = width as usize ;
1051+ let height = height as usize ;
1052+ let Some ( default_fill) = first_opaque_rgb ( rgba) else {
1053+ for pixel in rgba. chunks_exact_mut ( 4 ) {
1054+ pixel[ 3 ] = 255 ;
1055+ }
1056+ return ;
1057+ } ;
1058+
1059+ for y in 0 ..height {
1060+ let row_start = y * width * 4 ;
1061+ let row = & mut rgba[ row_start..row_start + width * 4 ] ;
1062+ let mut fill = first_opaque_rgb ( row) . unwrap_or ( default_fill) ;
1063+ for pixel in row. chunks_exact_mut ( 4 ) {
1064+ if pixel[ 3 ] == 255 {
1065+ fill = [ pixel[ 0 ] , pixel[ 1 ] , pixel[ 2 ] ] ;
1066+ continue ;
1067+ }
1068+ composite_pixel_over_rgb ( pixel, fill) ;
1069+ }
1070+ }
1071+ }
1072+
1073+ fn first_opaque_rgb ( rgba : & [ u8 ] ) -> Option < [ u8 ; 3 ] > {
1074+ rgba. chunks_exact ( 4 )
1075+ . find ( |pixel| pixel[ 3 ] == 255 )
1076+ . map ( |pixel| [ pixel[ 0 ] , pixel[ 1 ] , pixel[ 2 ] ] )
1077+ }
1078+
1079+ fn composite_pixel_over_rgb ( pixel : & mut [ u8 ] , background : [ u8 ; 3 ] ) {
1080+ let alpha = u32:: from ( pixel[ 3 ] ) ;
1081+ if alpha == 0 {
1082+ pixel[ 0 ] = background[ 0 ] ;
1083+ pixel[ 1 ] = background[ 1 ] ;
1084+ pixel[ 2 ] = background[ 2 ] ;
1085+ pixel[ 3 ] = 255 ;
1086+ return ;
1087+ }
1088+
1089+ for channel in 0 ..3 {
1090+ pixel[ channel] = ( ( u32:: from ( pixel[ channel] ) * alpha
1091+ + u32:: from ( background[ channel] ) * ( 255 - alpha)
1092+ + 127 )
1093+ / 255 ) as u8 ;
1094+ }
1095+ pixel[ 3 ] = 255 ;
1096+ }
1097+
10441098fn run_command_text < const N : usize > ( program : PathBuf , args : [ & str ; N ] ) -> Result < String , AppError > {
10451099 let output = run_command ( program, args) ?;
10461100 String :: from_utf8 ( output)
@@ -1732,7 +1786,7 @@ mod tests {
17321786
17331787 #[ test]
17341788 fn android_frame_orientation_keeps_matching_aspect ( ) {
1735- let rgba = vec ! [ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 ] ;
1789+ let rgba = vec ! [ 1 , 2 , 3 , 255 , 5 , 6 , 7 , 255 ] ;
17361790
17371791 let ( width, height, out) = normalize_android_frame_orientation (
17381792 1 ,
@@ -1749,6 +1803,27 @@ mod tests {
17491803 assert_eq ! ( out, rgba) ;
17501804 }
17511805
1806+ #[ test]
1807+ fn android_frame_alpha_flattens_transparent_edges_to_nearest_row_pixel ( ) {
1808+ let mut rgba = vec ! [ 0 , 0 , 0 , 0 , 10 , 20 , 30 , 255 , 40 , 50 , 60 , 255 , 0 , 0 , 0 , 0 ] ;
1809+
1810+ flatten_android_frame_alpha ( & mut rgba, 4 , 1 ) ;
1811+
1812+ assert_eq ! (
1813+ rgba,
1814+ vec![ 10 , 20 , 30 , 255 , 10 , 20 , 30 , 255 , 40 , 50 , 60 , 255 , 40 , 50 , 60 , 255 , ]
1815+ ) ;
1816+ }
1817+
1818+ #[ test]
1819+ fn android_frame_alpha_composites_partial_alpha ( ) {
1820+ let mut rgba = vec ! [ 100 , 50 , 0 , 255 , 200 , 200 , 200 , 128 ] ;
1821+
1822+ flatten_android_frame_alpha ( & mut rgba, 2 , 1 ) ;
1823+
1824+ assert_eq ! ( rgba, vec![ 100 , 50 , 0 , 255 , 150 , 125 , 100 , 255 ] ) ;
1825+ }
1826+
17521827 #[ test]
17531828 fn android_frame_orientation_rotates_reverse_orientations_180 ( ) {
17541829 let rgba = vec ! [
0 commit comments