V1.0: Class 1 fax — real-world 4-page send to external number confirmed

Core features:
- Class 1 T.30 protocol: full send/receive implementation
- HDLC: DLE-stuffing, FCS strip, USR5637 bit-reversal handling
- T.4 MH encoder/decoder (1728px A4 standard)
- Document pipeline: PDF (Ghostscript), PNG, TIFF input
- Width clamping: US Letter 1734px → 1728px fax standard
- Cover page: CJK rasterization (TW/CN/JP/EN), TIFF + HTML output
- OCR verification: Tesseract 5 with eng+chi_tra, CJK space-tolerant
- API server (axum): health, send, jobs, cover, retry, cancel
- Background worker: auto-poll queue, speed fallback, retry policy
- Modem detection, pool management

Real-world test results (2026-07-23):
- V90 → 25153038: 4 pages, V.17 12000 bps, 2:33 ✅
- USR5637 → 25153038: 4 pages, V.17 12000 bps, 2:26 ✅
- Both faxes confirmed received on remote machine

Tested: loopback (100% pixel match), multi-page, all input formats,
cover pages, OCR verify, API endpoints, worker processing.
13 unit tests pass, 0 new clippy warnings.
This commit is contained in:
Warren
2026-07-24 18:47:15 +08:00
parent d1e92b32fb
commit 55bca92691
155 changed files with 25024 additions and 916 deletions
+25 -2
View File
@@ -61,7 +61,7 @@ pub fn tiff_to_fax_document(tiff_data: &[u8]) -> Result<FaxDocument> {
let (width, height) = decoder.dimensions()
.map_err(|e| FaxError::document(format!("Failed to get TIFF dimensions: {}", e)))?;
let bytes_per_row = ((width + 7) / 8) as usize;
let bytes_per_row = width.div_ceil(8) as usize;
let mut pixels = vec![0u8; bytes_per_row * height as usize];
let mut result = decoder.read_image()
@@ -88,9 +88,32 @@ pub fn tiff_to_fax_document(tiff_data: &[u8]) -> Result<FaxDocument> {
}
}
// Clamp to A4 fax width (1728 pels). US Letter at 204 DPI = 1734.
let fax_width = width.min(1728);
// If wider than A4, re-encode pixels at the clamped width
let (pixels, width_pels) = if width > 1728 {
let bytes_per_row = fax_width.div_ceil(8) as usize;
let mut out = vec![0u8; bytes_per_row * height as usize];
for y in 0..height {
for x in 0..fax_width {
// img_data is 8-bit grayscale, one byte per pixel
let src_pixel = img_data[(y * width + x) as usize];
if src_pixel < 128 {
let dst_byte = y as usize * bytes_per_row + (x as usize / 8);
let dst_bit = 7 - (x % 8);
out[dst_byte] |= 1 << dst_bit;
}
}
}
(out, fax_width)
} else {
(pixels, width)
};
pages.push(Page {
pixels,
width_pels: width,
width_pels,
rows: height,
});