diff --git a/crates/ericrfb/examples/record.rs b/crates/ericrfb/examples/record.rs new file mode 100644 index 0000000..5e40f0c --- /dev/null +++ b/crates/ericrfb/examples/record.rs @@ -0,0 +1,108 @@ +use std::env; +use std::fs::{self, File}; +use std::io::BufWriter; +use std::path::Path; +use std::time::{Duration, Instant}; + +use ericrfb::handshake::Config; +use ericrfb::session::{ActiveSession, Event}; + +fn save_png(fb: &ericrfb::framebuffer::Framebuffer, path: &Path) { + let rgba = fb.to_rgba(); + let file = File::create(path).expect("cannot create PNG file"); + let bw = BufWriter::new(file); + let mut encoder = png::Encoder::new(bw, fb.width as u32, fb.height as u32); + encoder.set_color(png::ColorType::Rgba); + encoder.set_depth(png::BitDepth::Eight); + let mut writer = encoder.write_header().expect("png header failed"); + writer.write_image_data(&rgba).expect("png write failed"); +} + +fn main() { + let args: Vec = env::args().collect(); + + let host = args + .iter() + .position(|a| a == "--host") + .and_then(|i| args.get(i + 1)) + .expect("usage: --host --applet-id [--port ] [--duration ]"); + + let applet_id = args + .iter() + .position(|a| a == "--applet-id") + .and_then(|i| args.get(i + 1)) + .expect("usage: --host --applet-id [--port ] [--duration ]"); + + let port: u16 = args + .iter() + .position(|a| a == "--port") + .and_then(|i| args.get(i + 1)) + .and_then(|s| s.parse().ok()) + .unwrap_or(443); + + let duration_secs: u64 = args + .iter() + .position(|a| a == "--duration") + .and_then(|i| args.get(i + 1)) + .and_then(|s| s.parse().ok()) + .unwrap_or(30); + + let out_dir = Path::new("out"); + fs::create_dir_all(out_dir).expect("cannot create out/"); + + let cfg = Config::new(host, port, applet_id); + println!("Connecting to {}:{}...", cfg.host, cfg.port); + + // Request Hextile (5), CopyRect (1), Raw (0) + let mut session = ActiveSession::connect(&cfg, &[5, 1, 0]).expect("connect failed"); + println!( + "Connected: {}x{}, recording for {duration_secs}s...", + session.framebuffer.width, session.framebuffer.height + ); + + let start = Instant::now(); + let duration = Duration::from_secs(duration_secs); + let mut last_save = Instant::now() - Duration::from_secs(2); // force first save + let mut frame_count = 0u32; + + loop { + if start.elapsed() >= duration { + break; + } + + match session.process_one() { + Ok(Some(Event::FramebufferDirty)) => { + // Save at most 1 PNG per second + if last_save.elapsed() >= Duration::from_secs(1) { + let path = out_dir.join(format!("frame_{frame_count:04}.png")); + save_png(&session.framebuffer, &path); + println!( + "[{:.1}s] saved {}", + start.elapsed().as_secs_f64(), + path.display() + ); + frame_count += 1; + last_save = Instant::now(); + } + // Request next update + if let Err(e) = session.request_update() { + eprintln!("Error requesting update: {e}"); + break; + } + } + Ok(Some(Event::Resize { width, height })) => { + println!("Resized to {width}x{height}"); + } + Ok(Some(Event::Debug(s))) => { + eprintln!("[debug] {s}"); + } + Ok(Some(_)) | Ok(None) => {} + Err(e) => { + eprintln!("Error: {e}"); + break; + } + } + } + + println!("Done. Saved {frame_count} frames to {}/", out_dir.display()); +} diff --git a/crates/ericrfb/src/codec/hextile.rs b/crates/ericrfb/src/codec/hextile.rs new file mode 100644 index 0000000..22efd73 --- /dev/null +++ b/crates/ericrfb/src/codec/hextile.rs @@ -0,0 +1,148 @@ +use std::io::Read; + +use crate::framebuffer::Framebuffer; +use crate::proto::{self, read_exact, read_u8}; + +// Subencoding flag bits — ByteColorRFBRenderer.int(), line 192 +const RAW: u8 = 1; +const BACKGROUND_SPECIFIED: u8 = 2; +const FOREGROUND_SPECIFIED: u8 = 4; +const ANY_SUBRECTS: u8 = 8; +const SUBRECTS_COLOURED: u8 = 16; + +/// Decode a Hextile-encoded rectangle into the framebuffer. +/// +/// The rectangle is divided into 16x16 tiles (edge tiles may be smaller). +/// Background and foreground colors persist across tiles within one call. +/// +/// Reference: ByteColorRFBRenderer.int() line 169. +pub fn decode_hextile( + r: &mut impl Read, + fb: &mut Framebuffer, + rx: u16, + ry: u16, + rw: u16, + rh: u16, +) -> proto::Result<()> { + let mut bg: u8 = 0; + let mut fg: u8 = 0; + + let mut ty = ry; + while ty < ry + rh { + let tile_h = (ry + rh - ty).min(16); + let mut tx = rx; + while tx < rx + rw { + let tile_w = (rx + rw - tx).min(16); + let flags = read_u8(r)?; + + if flags & RAW != 0 { + // Raw tile: read tile_w * tile_h bytes + let size = tile_w as usize * tile_h as usize; + let data = read_exact(r, size)?; + fb.apply_raw(tx, ty, tile_w, tile_h, &data); + tx += 16; + continue; + } + + if flags & BACKGROUND_SPECIFIED != 0 { + bg = read_u8(r)?; + } + + // Fill tile with background + fb.fill_rect(tx, ty, tile_w, tile_h, bg); + + if flags & FOREGROUND_SPECIFIED != 0 { + fg = read_u8(r)?; + } + + if flags & ANY_SUBRECTS != 0 { + let num_subrects = read_u8(r)?; + let coloured = flags & SUBRECTS_COLOURED != 0; + + for _ in 0..num_subrects { + let color = if coloured { read_u8(r)? } else { fg }; + let xy = read_u8(r)?; + let wh = read_u8(r)?; + let sx = (xy >> 4) as u16; + let sy = (xy & 0x0F) as u16; + let sw = ((wh >> 4) + 1) as u16; + let sh = ((wh & 0x0F) + 1) as u16; + fb.fill_rect(tx + sx, ty + sy, sw, sh, color); + } + } + + tx += 16; + } + ty += 16; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Cursor; + + #[test] + fn test_hextile_raw_tile() { + let mut fb = Framebuffer::new(16, 16); + // One 16x16 tile, Raw subencoding + let mut data = vec![RAW]; // flags + data.extend_from_slice(&[0x42u8; 256]); // 16*16 raw pixels + let mut c = Cursor::new(data); + decode_hextile(&mut c, &mut fb, 0, 0, 16, 16).unwrap(); + assert_eq!(fb.pixels[0], 0x42); + assert_eq!(fb.pixels[255], 0x42); + } + + #[test] + fn test_hextile_bg_fill() { + let mut fb = Framebuffer::new(16, 16); + // One tile: background=0x09, no subrects + let data = vec![BACKGROUND_SPECIFIED, 0x09]; + let mut c = Cursor::new(data); + decode_hextile(&mut c, &mut fb, 0, 0, 16, 16).unwrap(); + assert!(fb.pixels.iter().all(|&p| p == 0x09)); + } + + #[test] + fn test_hextile_subrects_coloured() { + let mut fb = Framebuffer::new(16, 16); + // Background=0x00, 1 coloured subrect at (2,3) size 4x5 color 0xFF + let data = vec![ + BACKGROUND_SPECIFIED | ANY_SUBRECTS | SUBRECTS_COLOURED, + 0x00, // bg + 1, // num_subrects + 0xFF, // subrect color + 0x23, // xy: x=2, y=3 + 0x34, // wh: w=3+1=4, h=4+1=5 + ]; + let mut c = Cursor::new(data); + decode_hextile(&mut c, &mut fb, 0, 0, 16, 16).unwrap(); + assert_eq!(fb.pixels[0], 0x00); // background + assert_eq!(fb.pixels[3 * 16 + 2], 0xFF); // subrect at (2,3) + assert_eq!(fb.pixels[7 * 16 + 5], 0xFF); // subrect at (5,7) + assert_eq!(fb.pixels[8 * 16 + 2], 0x00); // below subrect + } + + #[test] + fn test_hextile_fg_subrects() { + let mut fb = Framebuffer::new(16, 16); + // Background=0x00, foreground=0xAA, 1 subrect at (0,0) size 2x2 + let data = vec![ + BACKGROUND_SPECIFIED | FOREGROUND_SPECIFIED | ANY_SUBRECTS, + 0x00, // bg + 0xAA, // fg + 1, // num_subrects + 0x00, // xy: x=0, y=0 + 0x11, // wh: w=1+1=2, h=1+1=2 + ]; + let mut c = Cursor::new(data); + decode_hextile(&mut c, &mut fb, 0, 0, 16, 16).unwrap(); + assert_eq!(fb.pixels[0], 0xAA); + assert_eq!(fb.pixels[1], 0xAA); + assert_eq!(fb.pixels[16], 0xAA); + assert_eq!(fb.pixels[17], 0xAA); + assert_eq!(fb.pixels[2], 0x00); + } +} diff --git a/crates/ericrfb/src/codec/mod.rs b/crates/ericrfb/src/codec/mod.rs new file mode 100644 index 0000000..f8fc5c6 --- /dev/null +++ b/crates/ericrfb/src/codec/mod.rs @@ -0,0 +1 @@ +pub mod hextile; diff --git a/crates/ericrfb/src/framebuffer.rs b/crates/ericrfb/src/framebuffer.rs index 36c1698..392b45e 100644 --- a/crates/ericrfb/src/framebuffer.rs +++ b/crates/ericrfb/src/framebuffer.rs @@ -40,6 +40,15 @@ impl Framebuffer { } } + /// Fill a rectangle with a single 8bpp color value. + pub fn fill_rect(&mut self, x: u16, y: u16, w: u16, h: u16, color: u8) { + let stride = self.width as usize; + for row in 0..h as usize { + let offset = (y as usize + row) * stride + x as usize; + self.pixels[offset..offset + w as usize].fill(color); + } + } + /// Copy a rectangle within the framebuffer (CopyRect encoding). /// Handles overlapping regions correctly. pub fn copy_rect(&mut self, src_x: u16, src_y: u16, dst_x: u16, dst_y: u16, w: u16, h: u16) { diff --git a/crates/ericrfb/src/lib.rs b/crates/ericrfb/src/lib.rs index 22dd7f0..5feec30 100644 --- a/crates/ericrfb/src/lib.rs +++ b/crates/ericrfb/src/lib.rs @@ -1,3 +1,4 @@ +pub mod codec; pub mod framebuffer; pub mod handshake; pub mod msg; diff --git a/crates/ericrfb/src/session.rs b/crates/ericrfb/src/session.rs index 94d3476..d2db585 100644 --- a/crates/ericrfb/src/session.rs +++ b/crates/ericrfb/src/session.rs @@ -1,6 +1,7 @@ use std::io::{BufReader, BufWriter}; use std::net::TcpStream; +use crate::codec::hextile; use crate::framebuffer::Framebuffer; use crate::handshake::{self, Config, ServerInit}; use crate::msg::{self, ServerMsg}; @@ -196,6 +197,17 @@ impl ActiveSession { self.framebuffer .copy_rect(src_x, src_y, hdr.x, hdr.y, hdr.w, hdr.h); } + 5 => { + // Hextile + hextile::decode_hextile( + &mut self.reader, + &mut self.framebuffer, + hdr.x, + hdr.y, + hdr.w, + hdr.h, + )?; + } other => { return Err(SessionError::UnsupportedEncoding(other)); }