codec/tight.rs: - Full Tight (encoding 7) decoder per ByteColorRFBRenderer.a() line 324 - Control byte: bottom 4 bits = zlib stream reset flags, top 4 bits = subencoding (0-15) - Subencoding 8: solid fill (1-byte color) - Subencoding 15: palette-indexed fill (selector + index) - Subencodings 4-7: filtered data with optional 2-color palette - Subencodings 10-13: reduced bit-depth packed (1/2/4 bpp) - Subencodings 0-3: raw 8bpp data - Data >= 12 bytes uses zlib compression with varint length - 4 persistent zlib streams with reset-on-flag logic - All 4 hardcoded sub-palettes ported as RGB332 indices: PALETTE_BW (2), PALETTE_GRAY4 (4), PALETTE_GRAY16 (16), PALETTE_COLOR16 (16 EGA-like colors) - Bit-depth unpackers: 1bpp, 2bpp, 4bpp (MSB-first) - 5 unit tests Updated examples to request [7, 5, 1, 0, -250] (Tight preferred). Tested against real OmniView: correct rendering with Tight encoding. 32 tests passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
89 lines
2.8 KiB
Rust
89 lines
2.8 KiB
Rust
use std::env;
|
|
use std::fs::File;
|
|
use std::io::BufWriter;
|
|
use std::path::Path;
|
|
|
|
use ericrfb::handshake::Config;
|
|
use ericrfb::session::{ActiveSession, Event};
|
|
|
|
fn main() {
|
|
let args: Vec<String> = env::args().collect();
|
|
|
|
let host = args
|
|
.iter()
|
|
.position(|a| a == "--host")
|
|
.and_then(|i| args.get(i + 1))
|
|
.expect("usage: --host <ip> --applet-id <token> [--port <port>] [--output <file.png>]");
|
|
|
|
let applet_id = args
|
|
.iter()
|
|
.position(|a| a == "--applet-id")
|
|
.and_then(|i| args.get(i + 1))
|
|
.expect("usage: --host <ip> --applet-id <token> [--port <port>] [--output <file.png>]");
|
|
|
|
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 output = args
|
|
.iter()
|
|
.position(|a| a == "--output")
|
|
.and_then(|i| args.get(i + 1).map(|s| s.as_str()))
|
|
.unwrap_or("frame.png");
|
|
|
|
let cfg = Config::new(host, port, applet_id);
|
|
println!("Connecting to {}:{}...", cfg.host, cfg.port);
|
|
|
|
// Request Tight (7), Hextile (5), CopyRect (1), Raw (0), cursor pseudo (-250)
|
|
let mut session = ActiveSession::connect(&cfg, &[7, 5, 1, 0, -250]).expect("connect failed");
|
|
println!(
|
|
"Connected: {}x{}, waiting for first frame...",
|
|
session.framebuffer.width, session.framebuffer.height
|
|
);
|
|
|
|
// Process messages until we get a FramebufferDirty event
|
|
loop {
|
|
match session.process_one() {
|
|
Ok(Some(Event::FramebufferDirty)) => {
|
|
println!("Got framebuffer update, saving to {output}");
|
|
break;
|
|
}
|
|
Ok(Some(Event::Resize { width, height })) => {
|
|
println!("Resized to {width}x{height}");
|
|
}
|
|
Ok(Some(Event::Debug(s))) => {
|
|
eprintln!("[debug] {s}");
|
|
}
|
|
Ok(Some(Event::RfbCommand(k, v))) => {
|
|
eprintln!("[rfb-cmd] {k}={v}");
|
|
}
|
|
Ok(Some(_)) => {}
|
|
Ok(None) => {}
|
|
Err(e) => {
|
|
eprintln!("Error: {e}");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Write PNG
|
|
let rgba = session.framebuffer.to_rgba();
|
|
let w = session.framebuffer.width as u32;
|
|
let h = session.framebuffer.height as u32;
|
|
|
|
let path = Path::new(output);
|
|
let file = File::create(path).expect("cannot create output file");
|
|
let bw = BufWriter::new(file);
|
|
|
|
let mut encoder = png::Encoder::new(bw, w, h);
|
|
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");
|
|
|
|
println!("Saved {w}x{h} frame to {}", path.display());
|
|
}
|