Initial commit: telfax - Rust Fax Server
- Modem driver (serialport) with AT command interface - Class 1 fax protocol (T.30 session, HDLC framing) - T.4/T.6 image codec (fax crate integration) - Document conversion (image to fax, TIFF-F output) - REST API (axum) for fax job management - SQLite/InMemory job queue - CLI interface (detect/serve/send/test) - Integration tests passing
This commit is contained in:
27
src/fax/class1/mod.rs
Normal file
27
src/fax/class1/mod.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
pub mod send;
|
||||
pub mod recv;
|
||||
pub mod session;
|
||||
|
||||
pub use send::Class1Send;
|
||||
pub use recv::Class1Recv;
|
||||
pub use session::T30Session;
|
||||
|
||||
use crate::modem::driver::ModemDriver;
|
||||
|
||||
pub struct Class1Session<'a> {
|
||||
driver: &'a mut ModemDriver,
|
||||
}
|
||||
|
||||
impl<'a> Class1Session<'a> {
|
||||
pub fn new(driver: &'a mut ModemDriver) -> Self {
|
||||
Self { driver }
|
||||
}
|
||||
|
||||
pub fn sender(self) -> Class1Send<'a> {
|
||||
Class1Send::new(self.driver)
|
||||
}
|
||||
|
||||
pub fn receiver(self) -> Class1Recv<'a> {
|
||||
Class1Recv::new(self.driver)
|
||||
}
|
||||
}
|
||||
35
src/fax/class1/recv.rs
Normal file
35
src/fax/class1/recv.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use crate::error::{FaxError, Result};
|
||||
use crate::fax::class1::session::{T30Event, T30Session};
|
||||
use crate::modem::driver::ModemDriver;
|
||||
|
||||
pub struct Class1Recv<'a> {
|
||||
driver: &'a mut ModemDriver,
|
||||
session: T30Session,
|
||||
}
|
||||
|
||||
impl<'a> Class1Recv<'a> {
|
||||
pub fn new(driver: &'a mut ModemDriver) -> Self {
|
||||
Self {
|
||||
driver,
|
||||
session: T30Session::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn receive_fax(&mut self) -> Result<Vec<u8>> {
|
||||
let mut at = crate::modem::at::AtChannel::new(self.driver);
|
||||
|
||||
at.send_command("AT+FCLASS=1", 2000)?;
|
||||
at.send_command("ATS0=1", 2000)?;
|
||||
|
||||
tracing::info!("Waiting for incoming call...");
|
||||
let _ring = self.driver.read_until(b"RING\r\n", 120_000)?;
|
||||
tracing::info!("RING received");
|
||||
|
||||
let _conn = self.driver.read_until(b"CONNECT\r\n", 60000)?;
|
||||
tracing::info!("Incoming fax connected");
|
||||
|
||||
self.session.transition(T30Event::Connected);
|
||||
|
||||
Err(FaxError::protocol("Receive not yet fully implemented"))
|
||||
}
|
||||
}
|
||||
219
src/fax/class1/send.rs
Normal file
219
src/fax/class1/send.rs
Normal file
@@ -0,0 +1,219 @@
|
||||
use crate::error::{FaxError, Result};
|
||||
use crate::fax::class1::session::{T30Event, T30Session};
|
||||
use crate::fax::hdlc::{build_hdlc_frame, parse_hdlc_frame};
|
||||
use crate::fax::negotiate::{DataRate, FaxCapabilities};
|
||||
use crate::modem::driver::ModemDriver;
|
||||
|
||||
pub struct Class1Send<'a> {
|
||||
driver: &'a mut ModemDriver,
|
||||
session: T30Session,
|
||||
#[allow(dead_code)]
|
||||
caps: FaxCapabilities,
|
||||
data_rate: DataRate,
|
||||
}
|
||||
|
||||
impl<'a> Class1Send<'a> {
|
||||
pub fn new(driver: &'a mut ModemDriver) -> Self {
|
||||
Self {
|
||||
driver,
|
||||
session: T30Session::new(),
|
||||
caps: FaxCapabilities::default(),
|
||||
data_rate: DataRate::V17_14400,
|
||||
}
|
||||
}
|
||||
|
||||
fn at(&mut self) -> crate::modem::at::AtChannel<'_> {
|
||||
crate::modem::at::AtChannel::new(self.driver)
|
||||
}
|
||||
|
||||
pub fn send_fax(
|
||||
&mut self,
|
||||
number: &str,
|
||||
page_data: &[u8],
|
||||
_page_width: u32,
|
||||
_page_rows: u32,
|
||||
) -> Result<()> {
|
||||
self.enter_class1()?;
|
||||
self.set_speaker_volume()?;
|
||||
self.dial(number)?;
|
||||
|
||||
self.session.transition(T30Event::Connected);
|
||||
|
||||
let dis_frame = self.receive_hdlc_frame()?;
|
||||
tracing::info!("Received DIS frame ({} bytes)", dis_frame.len());
|
||||
|
||||
let parsed_dis = parse_hdlc_frame(&dis_frame)?;
|
||||
tracing::info!(control = parsed_dis.control, "Parsed DIS");
|
||||
self.negotiate_caps(&parsed_dis)?;
|
||||
|
||||
let dcs_data = self.build_dcs();
|
||||
let dcs_frame = build_hdlc_frame(0x28, &dcs_data);
|
||||
self.transmit_hdlc_frame(&dcs_frame)?;
|
||||
tracing::info!("Sent DCS");
|
||||
|
||||
let rate_val = self.data_rate.to_modulation_value();
|
||||
self.transmit_tcf(rate_val)?;
|
||||
tracing::info!("Sent TCF");
|
||||
|
||||
let cfr_frame = self.receive_hdlc_frame()?;
|
||||
let parsed_cfr = parse_hdlc_frame(&cfr_frame)?;
|
||||
if parsed_cfr.control != 0x21 {
|
||||
return Err(FaxError::protocol("Expected CFR frame"));
|
||||
}
|
||||
tracing::info!("Received CFR");
|
||||
self.session.transition(T30Event::CfrReceived);
|
||||
|
||||
self.transmit_page_data(rate_val, page_data)?;
|
||||
self.session.transition(T30Event::PageSent);
|
||||
|
||||
self.send_eop()?;
|
||||
let mcf_frame = self.receive_hdlc_frame()?;
|
||||
let parsed_mcf = parse_hdlc_frame(&mcf_frame)?;
|
||||
tracing::info!(control = parsed_mcf.control, "Post-page response received");
|
||||
self.session.transition(T30Event::McfReceived);
|
||||
|
||||
self.send_dcn()?;
|
||||
self.hang_up()?;
|
||||
self.session.transition(T30Event::DcnSent);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn enter_class1(&mut self) -> Result<()> {
|
||||
self.at().send_command("AT+FCLASS=1", 2000)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_speaker_volume(&mut self) -> Result<()> {
|
||||
self.at().send_command("ATM1L2", 1000).ok();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn dial(&mut self, number: &str) -> Result<()> {
|
||||
self.driver.drain()?;
|
||||
let cmd = format!("ATDT{}\r", number);
|
||||
self.driver.write_raw(cmd.as_bytes())?;
|
||||
self.driver.flush()?;
|
||||
|
||||
let response = self.driver.read_until(b"\r\n", 60000)?;
|
||||
let text = String::from_utf8_lossy(&response);
|
||||
tracing::info!(response = %text, "Dial response");
|
||||
|
||||
if text.contains("NO CARRIER") || text.contains("BUSY") {
|
||||
return Err(FaxError::NoCarrier);
|
||||
}
|
||||
if !text.contains("CONNECT") {
|
||||
return Err(FaxError::modem(format!("Unexpected dial response: {}", text)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn receive_hdlc_frame(&mut self) -> Result<Vec<u8>> {
|
||||
self.driver.write_raw(b"AT+FRH=3\r")?;
|
||||
|
||||
let response = self.driver.read_until(b"\r\n", 15000)?;
|
||||
let text = String::from_utf8_lossy(&response);
|
||||
tracing::debug!(response = %text, "FRH initial");
|
||||
|
||||
if text.contains("NO CARRIER") || text.contains("ERROR") {
|
||||
return Err(FaxError::NoCarrier);
|
||||
}
|
||||
|
||||
let result = self.driver.read_until(b"OK\r\n", 10000)?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn transmit_hdlc_frame(&mut self, frame: &[u8]) -> Result<()> {
|
||||
self.driver.write_raw(b"AT+FTH=3\r")?;
|
||||
|
||||
let response = self.driver.read_until(b"CONNECT\r\n", 10000)?;
|
||||
let text = String::from_utf8_lossy(&response);
|
||||
if text.contains("ERROR") {
|
||||
return Err(FaxError::modem("Failed to start HDLC transmit"));
|
||||
}
|
||||
|
||||
self.driver.write_raw(frame)?;
|
||||
self.driver.flush()?;
|
||||
|
||||
let result = self.driver.read_until(b"OK\r\n", 5000)?;
|
||||
let result_text = String::from_utf8_lossy(&result);
|
||||
if result_text.contains("ERROR") {
|
||||
return Err(FaxError::modem("HDLC transmit failed"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn transmit_tcf(&mut self, rate_val: u32) -> Result<()> {
|
||||
let cmd = format!("AT+FTM={}\r", rate_val);
|
||||
self.driver.write_raw(cmd.as_bytes())?;
|
||||
|
||||
let response = self.driver.read_until(b"CONNECT\r\n", 10000)?;
|
||||
let text = String::from_utf8_lossy(&response);
|
||||
if text.contains("ERROR") {
|
||||
return Err(FaxError::modem("Failed to start TCF transmit"));
|
||||
}
|
||||
|
||||
let tcf_data = vec![0u8; 2000];
|
||||
self.driver.write_raw(&tcf_data)?;
|
||||
self.driver.flush()?;
|
||||
|
||||
let result = self.driver.read_until(b"OK\r\n", 10000)?;
|
||||
let result_text = String::from_utf8_lossy(&result);
|
||||
if result_text.contains("ERROR") {
|
||||
return Err(FaxError::TrainingFailed);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn transmit_page_data(&mut self, rate_val: u32, data: &[u8]) -> Result<()> {
|
||||
let cmd = format!("AT+FTM={}\r", rate_val);
|
||||
self.driver.write_raw(cmd.as_bytes())?;
|
||||
|
||||
let response = self.driver.read_until(b"CONNECT\r\n", 10000)?;
|
||||
let text = String::from_utf8_lossy(&response);
|
||||
if text.contains("ERROR") {
|
||||
return Err(FaxError::modem("Failed to start page data transmit"));
|
||||
}
|
||||
|
||||
self.driver.write_raw(data)?;
|
||||
self.driver.write_raw(b"\x01\x01\x01\x01")?;
|
||||
self.driver.flush()?;
|
||||
|
||||
let result = self.driver.read_until(b"OK\r\n", 15000)?;
|
||||
let result_text = String::from_utf8_lossy(&result);
|
||||
if result_text.contains("ERROR") {
|
||||
return Err(FaxError::protocol("Page transmit failed"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn send_eop(&mut self) -> Result<()> {
|
||||
let eop_data = build_hdlc_frame(0x42, &[]);
|
||||
self.transmit_hdlc_frame(&eop_data)
|
||||
}
|
||||
|
||||
fn send_dcn(&mut self) -> Result<()> {
|
||||
let dcn_data = build_hdlc_frame(0x62, &[]);
|
||||
self.transmit_hdlc_frame(&dcn_data)
|
||||
}
|
||||
|
||||
fn hang_up(&mut self) -> Result<()> {
|
||||
self.driver.write_raw(b"ATH\r")?;
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
self.driver.drain()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn negotiate_caps(&mut self, _dis_frame: &crate::fax::hdlc::HdlcFrame) -> Result<()> {
|
||||
self.data_rate = DataRate::V17_14400;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_dcs(&self) -> Vec<u8> {
|
||||
let mut dcs = Vec::new();
|
||||
dcs.push(0x00);
|
||||
dcs.push(0x02);
|
||||
dcs.push(0x05);
|
||||
dcs
|
||||
}
|
||||
}
|
||||
103
src/fax/class1/session.rs
Normal file
103
src/fax/class1/session.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
/// T.30 session state machine for Group 3 fax.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum T30State {
|
||||
Idle,
|
||||
Dialing,
|
||||
HandshakeTransmitting, // Phase B - sending DIS/DCS
|
||||
HandshakeReceiving, // Phase B - receiving DIS/DCS
|
||||
DcsSent, // DCS has been sent, waiting for CFR
|
||||
CfrReceived, // Received CFR, ready to send pages
|
||||
PageTransmitting, // Phase C - sending page data
|
||||
PageReceiving,
|
||||
PostPageHandshake, // Phase D - MCF, EOP, MPS
|
||||
Disconnecting, // Phase E - DCN
|
||||
Completed,
|
||||
Error,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum T30Event {
|
||||
Connected,
|
||||
DisReceived,
|
||||
DisSent,
|
||||
DcsReceived,
|
||||
DcsSent,
|
||||
TcfSent,
|
||||
TcfReceived,
|
||||
CfrReceived,
|
||||
CfrSent,
|
||||
PageSent,
|
||||
PageReceived,
|
||||
McfReceived,
|
||||
McfSent,
|
||||
EopReceived,
|
||||
EopSent,
|
||||
DcnReceived,
|
||||
DcnSent,
|
||||
Timeout,
|
||||
Error,
|
||||
Retrain,
|
||||
}
|
||||
|
||||
pub struct T30Session {
|
||||
state: T30State,
|
||||
retry_count: u8,
|
||||
max_retries: u8,
|
||||
}
|
||||
|
||||
impl T30Session {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
state: T30State::Idle,
|
||||
retry_count: 0,
|
||||
max_retries: 3,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn state(&self) -> T30State {
|
||||
self.state
|
||||
}
|
||||
|
||||
pub fn start_receive(&mut self) {
|
||||
self.state = T30State::HandshakeReceiving;
|
||||
}
|
||||
|
||||
pub fn transition(&mut self, event: T30Event) {
|
||||
match (self.state, event) {
|
||||
// Send flow
|
||||
(T30State::Idle, T30Event::Connected) => {
|
||||
// Default to transmitting; caller can override with start_receive()
|
||||
self.state = T30State::HandshakeTransmitting;
|
||||
}
|
||||
(T30State::HandshakeTransmitting, T30Event::DisReceived) => {
|
||||
self.state = T30State::DcsSent;
|
||||
}
|
||||
(T30State::DcsSent, T30Event::CfrReceived) => self.state = T30State::PageTransmitting,
|
||||
(T30State::PageTransmitting, T30Event::PageSent) => self.state = T30State::PostPageHandshake,
|
||||
(T30State::PostPageHandshake, T30Event::McfReceived) => self.state = T30State::Disconnecting,
|
||||
(T30State::PostPageHandshake, T30Event::EopReceived) => self.state = T30State::Disconnecting,
|
||||
(T30State::Disconnecting, T30Event::DcnSent) => self.state = T30State::Completed,
|
||||
|
||||
// Receive flow
|
||||
(T30State::HandshakeReceiving, T30Event::DcsReceived) => self.state = T30State::PageReceiving,
|
||||
(T30State::PageReceiving, T30Event::PageReceived) => self.state = T30State::PostPageHandshake,
|
||||
(T30State::PostPageHandshake, T30Event::McfSent) => self.state = T30State::Disconnecting,
|
||||
(T30State::Disconnecting, T30Event::DcnReceived) => self.state = T30State::Completed,
|
||||
|
||||
// Error handling
|
||||
(_, T30Event::Timeout) | (_, T30Event::Error) => {
|
||||
if self.retry_count < self.max_retries {
|
||||
self.retry_count += 1;
|
||||
// Stay in same state for retry
|
||||
} else {
|
||||
self.state = T30State::Error;
|
||||
}
|
||||
}
|
||||
|
||||
_ => {
|
||||
// Unexpected transition - log and set error
|
||||
self.state = T30State::Error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
150
src/fax/hdlc.rs
Normal file
150
src/fax/hdlc.rs
Normal file
@@ -0,0 +1,150 @@
|
||||
use crate::error::{FaxError, Result};
|
||||
|
||||
const HDLC_FLAG: u8 = 0x7E;
|
||||
const HDLC_ESC: u8 = 0x7D;
|
||||
|
||||
/// T.30 HDLC frame types used in fax protocol.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum FrameType {
|
||||
/// Digital Identification Signal (DIS) - Called station capabilities
|
||||
Dis,
|
||||
/// Digital Command Signal (DCS) - Transmitting station capabilities
|
||||
Dcs,
|
||||
/// Training Check Frame
|
||||
Tcf,
|
||||
/// Confirmation to Receive (CFR) - Ready for pages
|
||||
Cfr,
|
||||
/// Frame Check Sequence error / invalid
|
||||
Invalid,
|
||||
/// Unknown frame type
|
||||
Other(u8),
|
||||
}
|
||||
|
||||
impl FrameType {
|
||||
pub fn from_control(ctrl: u8) -> Self {
|
||||
match ctrl {
|
||||
0x08 => FrameType::Dis,
|
||||
0x28 => FrameType::Dcs,
|
||||
0x41 => FrameType::Tcf,
|
||||
0x21 => FrameType::Cfr,
|
||||
_ => FrameType::Other(ctrl),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HdlcFrame {
|
||||
pub address: u8,
|
||||
pub control: u8,
|
||||
pub information: Vec<u8>,
|
||||
pub fcs: u16,
|
||||
}
|
||||
|
||||
/// Parse HDLC frames from a byte stream.
|
||||
pub fn parse_hdlc_frame(data: &[u8]) -> Result<HdlcFrame> {
|
||||
// Remove leading flags
|
||||
let start = data.iter().position(|&b| b != HDLC_FLAG)
|
||||
.ok_or_else(|| FaxError::Hdlc("No HDLC frame found".into()))?;
|
||||
|
||||
let mut frame_data = Vec::new();
|
||||
let mut i = start;
|
||||
let mut escaped = false;
|
||||
|
||||
while i < data.len() {
|
||||
let byte = data[i];
|
||||
if byte == HDLC_FLAG {
|
||||
if !frame_data.is_empty() {
|
||||
break;
|
||||
}
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if byte == HDLC_ESC {
|
||||
escaped = true;
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if escaped {
|
||||
frame_data.push(byte ^ 0x20);
|
||||
escaped = false;
|
||||
} else {
|
||||
frame_data.push(byte);
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
if frame_data.len() < 4 {
|
||||
return Err(FaxError::Hdlc(format!(
|
||||
"Frame too short: {} bytes",
|
||||
frame_data.len()
|
||||
)));
|
||||
}
|
||||
|
||||
let fcs_bytes = &frame_data[frame_data.len() - 2..];
|
||||
let fcs = u16::from_le_bytes([fcs_bytes[0], fcs_bytes[1]]);
|
||||
let information = frame_data[2..frame_data.len() - 2].to_vec();
|
||||
|
||||
Ok(HdlcFrame {
|
||||
address: frame_data[0],
|
||||
control: frame_data[1],
|
||||
information,
|
||||
fcs,
|
||||
})
|
||||
}
|
||||
|
||||
/// Build HDLC frame bytes for a T.30 fax frame.
|
||||
pub fn build_hdlc_frame(control: u8, information: &[u8]) -> Vec<u8> {
|
||||
let mut frame = Vec::new();
|
||||
frame.push(0xFF); // address (broadcast)
|
||||
frame.push(control); // control field
|
||||
frame.extend_from_slice(information);
|
||||
|
||||
let fcs = compute_fcs(&frame);
|
||||
frame.extend_from_slice(&fcs.to_le_bytes());
|
||||
|
||||
// Bit-stuff and wrap in flags
|
||||
let mut stuffed = Vec::new();
|
||||
stuffed.push(HDLC_FLAG);
|
||||
for &byte in &frame {
|
||||
if byte == HDLC_FLAG || byte == HDLC_ESC {
|
||||
stuffed.push(HDLC_ESC);
|
||||
stuffed.push(byte ^ 0x20);
|
||||
} else {
|
||||
stuffed.push(byte);
|
||||
}
|
||||
}
|
||||
stuffed.push(HDLC_FLAG);
|
||||
stuffed
|
||||
}
|
||||
|
||||
/// Compute CRC-CCITT (16-bit FCS) for HDLC frames.
|
||||
fn compute_fcs(data: &[u8]) -> u16 {
|
||||
let mut crc: u16 = 0xFFFF;
|
||||
for &byte in data {
|
||||
crc ^= (byte as u16) << 8;
|
||||
for _ in 0..8 {
|
||||
if crc & 0x8000 != 0 {
|
||||
crc = (crc << 1) ^ 0x1021;
|
||||
} else {
|
||||
crc <<= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
!crc
|
||||
}
|
||||
|
||||
/// Check if an HDLC frame has a valid FCS.
|
||||
pub fn verify_fcs(frame: &[u8]) -> bool {
|
||||
let mut crc: u16 = 0xFFFF;
|
||||
for &byte in frame {
|
||||
crc ^= (byte as u16) << 8;
|
||||
for _ in 0..8 {
|
||||
if crc & 0x8000 != 0 {
|
||||
crc = (crc << 1) ^ 0x1021;
|
||||
} else {
|
||||
crc <<= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
crc == 0x1D0F
|
||||
}
|
||||
7
src/fax/mod.rs
Normal file
7
src/fax/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
pub mod t4;
|
||||
pub mod hdlc;
|
||||
pub mod negotiate;
|
||||
pub mod class1;
|
||||
|
||||
pub use class1::Class1Session;
|
||||
pub use t4::T4Codec;
|
||||
91
src/fax/negotiate.rs
Normal file
91
src/fax/negotiate.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FaxCapabilities {
|
||||
pub resolution: Resolution,
|
||||
pub page_width: PageWidth,
|
||||
pub page_length: PageLength,
|
||||
pub data_rate: DataRate,
|
||||
pub ecm: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Resolution {
|
||||
Standard, // 98 lpi, 204 dpi
|
||||
Fine, // 196 lpi, 204 dpi
|
||||
SuperFine, // 392 lpi, 204 dpi
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PageWidth {
|
||||
A4, // 1728 pels
|
||||
A3, // 2432 pels
|
||||
B4, // 2048 pels
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PageLength {
|
||||
A4, // 297mm
|
||||
Unlimited,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum DataRate {
|
||||
V27ter2400,
|
||||
V27ter4800,
|
||||
V29_7200,
|
||||
V29_9600,
|
||||
V17_7200,
|
||||
V17_9600,
|
||||
V17_12000,
|
||||
V17_14400,
|
||||
}
|
||||
|
||||
impl DataRate {
|
||||
pub fn to_modulation_value(&self) -> u32 {
|
||||
match self {
|
||||
DataRate::V27ter2400 => 24,
|
||||
DataRate::V27ter4800 => 48,
|
||||
DataRate::V29_7200 => 72,
|
||||
DataRate::V29_9600 => 96,
|
||||
DataRate::V17_7200 => 73,
|
||||
DataRate::V17_9600 => 97,
|
||||
DataRate::V17_12000 => 121,
|
||||
DataRate::V17_14400 => 145,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_modulation_value(val: u32) -> Option<Self> {
|
||||
match val {
|
||||
24 => Some(DataRate::V27ter2400),
|
||||
48 => Some(DataRate::V27ter4800),
|
||||
72 => Some(DataRate::V29_7200),
|
||||
96 => Some(DataRate::V29_9600),
|
||||
73 => Some(DataRate::V17_7200),
|
||||
97 => Some(DataRate::V17_9600),
|
||||
121 => Some(DataRate::V17_12000),
|
||||
145 => Some(DataRate::V17_14400),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn max_for_modem(supported_rates: &[u32]) -> Option<Self> {
|
||||
let priority = [145, 121, 97, 73, 96, 72, 48, 24];
|
||||
for rate in &priority {
|
||||
if supported_rates.contains(rate) {
|
||||
return Self::from_modulation_value(*rate);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FaxCapabilities {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
resolution: Resolution::Fine,
|
||||
page_width: PageWidth::A4,
|
||||
page_length: PageLength::A4,
|
||||
data_rate: DataRate::V17_14400,
|
||||
ecm: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
106
src/fax/t4.rs
Normal file
106
src/fax/t4.rs
Normal file
@@ -0,0 +1,106 @@
|
||||
use crate::error::{FaxError, Result};
|
||||
|
||||
pub struct T4Codec;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct T4Page {
|
||||
pub data: Vec<u8>,
|
||||
pub width_pels: u32,
|
||||
pub rows: u32,
|
||||
pub encoding: T4Encoding,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum T4Encoding {
|
||||
Group3MH,
|
||||
Group3MR,
|
||||
Group4MMR,
|
||||
}
|
||||
|
||||
impl T4Codec {
|
||||
/// Decode T.4/T.6 data into raw 1-bit pixel data (MSB-first, row-major).
|
||||
/// `transitions_to_pixels` converts run-length transitions to pixel data.
|
||||
fn transitions_to_pixels(
|
||||
transitions: &[u16],
|
||||
width_pels: u32,
|
||||
row_buf: &mut [u8],
|
||||
) {
|
||||
row_buf.fill(0);
|
||||
let mut is_black = false;
|
||||
let mut prev = 0u32;
|
||||
|
||||
for &t in transitions {
|
||||
let t = t as u32;
|
||||
if t > width_pels {
|
||||
break;
|
||||
}
|
||||
if is_black {
|
||||
for p in prev..t.min(width_pels) {
|
||||
let byte_idx = (p / 8) as usize;
|
||||
let bit_idx = 7 - (p % 8);
|
||||
if byte_idx < row_buf.len() {
|
||||
row_buf[byte_idx] |= 1 << bit_idx;
|
||||
}
|
||||
}
|
||||
}
|
||||
prev = t;
|
||||
is_black = !is_black;
|
||||
}
|
||||
// If ends on black, fill to end
|
||||
if is_black {
|
||||
for p in prev..width_pels {
|
||||
let byte_idx = (p / 8) as usize;
|
||||
let bit_idx = 7 - (p % 8);
|
||||
if byte_idx < row_buf.len() {
|
||||
row_buf[byte_idx] |= 1 << bit_idx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decode(data: &[u8], width_pels: u32, rows: u32, encoding: T4Encoding) -> Result<Vec<u8>> {
|
||||
let bytes_per_row = ((width_pels + 7) / 8) as usize;
|
||||
let mut pixels = vec![0u8; bytes_per_row * rows as usize];
|
||||
let data: Vec<u8> = data.to_vec();
|
||||
|
||||
match encoding {
|
||||
T4Encoding::Group3MH | T4Encoding::Group3MR => {
|
||||
let mut row_idx = 0u32;
|
||||
let result = fax::decoder::decode_g3(data.into_iter(), |transitions: &[u16]| {
|
||||
if (row_idx as usize) < pixels.len() / bytes_per_row {
|
||||
let start = row_idx as usize * bytes_per_row;
|
||||
let end = (start + bytes_per_row).min(pixels.len());
|
||||
let row_buf = &mut pixels[start..end];
|
||||
Self::transitions_to_pixels(transitions, width_pels, row_buf);
|
||||
}
|
||||
row_idx += 1;
|
||||
});
|
||||
match result {
|
||||
Some(()) => Ok(pixels),
|
||||
None => Err(FaxError::t4("Group 3 decode returned None")),
|
||||
}
|
||||
}
|
||||
T4Encoding::Group4MMR => {
|
||||
let mut row_idx = 0u32;
|
||||
let result = fax::decoder::decode_g4(
|
||||
data.into_iter(),
|
||||
width_pels as u16,
|
||||
None,
|
||||
|transitions: &[u16]| {
|
||||
if (row_idx as usize) < pixels.len() / bytes_per_row {
|
||||
let start = row_idx as usize * bytes_per_row;
|
||||
let end = (start + bytes_per_row).min(pixels.len());
|
||||
let row_buf = &mut pixels[start..end];
|
||||
Self::transitions_to_pixels(transitions, width_pels, row_buf);
|
||||
}
|
||||
row_idx += 1;
|
||||
},
|
||||
);
|
||||
match result {
|
||||
Some(()) => Ok(pixels),
|
||||
None => Err(FaxError::t4("Group 4 decode returned None")),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user