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:
Warren
2026-07-01 09:06:41 +08:00
commit 13438289fe
28 changed files with 4615 additions and 0 deletions
+106
View 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")),
}
}
}
}
}