Add cover page support with Chinese font rendering
Features:
- Cover page generation using Python/Pillow with Apple PingFang font
- CLI commands: cover-create, job-cover for cover page management
- API endpoints: PUT /api/fax/jobs/{id}/cover for cover modification
- Send fax with cover page: --to, --from, --subject, --note options
- Multi-page TIFF parsing for PDF conversion (fix: all pages now sent)
- Preview HTML generation with pan/zoom support
- OCR verification for cover page content
- Class 2 receive support for incoming faxes
Fixes:
- Modem initialization before Class 2 mode entry
- Dial timeout handling with retry logic
- Multi-page fax transmission between pages
- Chinese character rendering in cover pages (Apple native fonts)
This commit is contained in:
+92
-25
@@ -51,6 +51,10 @@ impl<'a> Class2Send<'a> {
|
||||
pub fn send_fax(&mut self, number: &str, pages: &[Page]) -> Result<()> {
|
||||
tracing::info!("Starting Class 2 fax to {}", number);
|
||||
|
||||
// Initialize modem first
|
||||
self.at().send_command("AT", 3000)?;
|
||||
tracing::debug!("Modem initialized");
|
||||
|
||||
// Enter Class 2 mode
|
||||
self.at().send_command(Class2Commands::ENTER_CLASS2, 5000)?;
|
||||
tracing::info!("Entered Class 2 mode");
|
||||
@@ -60,14 +64,18 @@ impl<'a> Class2Send<'a> {
|
||||
self.at().send_command(&station_cmd, 3000)?;
|
||||
tracing::info!("Station ID set: {}", self.station_id);
|
||||
|
||||
// Set header
|
||||
// Set header - may not be supported on some modems
|
||||
let header_cmd = Class2Commands::set_header(&self.header);
|
||||
self.at().send_command(&header_cmd, 3000)?;
|
||||
if self.at().send_command(&header_cmd, 3000).is_err() {
|
||||
tracing::warn!("Header command not supported, skipping");
|
||||
}
|
||||
|
||||
// Set capabilities
|
||||
// Set capabilities - skip if not supported
|
||||
let speed = if self.params.speed > 0 { self.params.speed } else { BR_V17_14400 };
|
||||
let dis_cmd = Class2Commands::set_dis_params(self.params.resolution, speed, self.params.ecm);
|
||||
self.at().send_command(&dis_cmd, 3000)?;
|
||||
if self.at().send_command(&dis_cmd, 3000).is_err() {
|
||||
tracing::warn!("DIS params command not supported, using defaults");
|
||||
}
|
||||
|
||||
// Dial
|
||||
self.dial(number)?;
|
||||
@@ -94,25 +102,64 @@ impl<'a> Class2Send<'a> {
|
||||
self.driver.write_raw(cmd.as_bytes())?;
|
||||
self.driver.write_raw(b"\r")?;
|
||||
self.driver.flush()?;
|
||||
tracing::info!("Dialing: {}", cmd);
|
||||
|
||||
// Wait for CONNECT or response
|
||||
let response = self.driver.read_until(b"\r\n", 60000)?;
|
||||
let text = String::from_utf8_lossy(&response);
|
||||
tracing::info!("Dial response: {}", text);
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(60);
|
||||
|
||||
// Check for successful connection
|
||||
if text.contains("NO CARRIER") || text.contains("BUSY") {
|
||||
return Err(FaxError::NoCarrier);
|
||||
// Read lines until CONNECT or error
|
||||
while std::time::Instant::now() < deadline {
|
||||
let line = match self.driver.read_until(b"\r\n", 5000) {
|
||||
Ok(l) => l,
|
||||
Err(crate::error::FaxError::Timeout { .. }) => continue,
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
let text = String::from_utf8_lossy(&line).trim().to_string();
|
||||
|
||||
if text.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
tracing::info!("Dial response: {}", text);
|
||||
|
||||
// Skip command echo
|
||||
if text.starts_with("ATDT") || text.starts_with("ATD") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Error conditions
|
||||
if text.contains("NO CARRIER") || text.contains("BUSY") {
|
||||
return Err(FaxError::NoCarrier);
|
||||
}
|
||||
if text.contains("ERROR") {
|
||||
return Err(FaxError::modem("Dial command failed"));
|
||||
}
|
||||
if text.contains("+FHNG:") {
|
||||
return Err(FaxError::protocol(format!("Fax hangup: {}", text)));
|
||||
}
|
||||
|
||||
// Connection established
|
||||
if text.contains("CONNECT") {
|
||||
tracing::info!("Connected!");
|
||||
// Read remaining session info (e.g. +FCON)
|
||||
let extra = self.driver.read_all(2000)?;
|
||||
let extra_text = String::from_utf8_lossy(&extra);
|
||||
if !extra_text.trim().is_empty() {
|
||||
tracing::info!("Session info: {}", extra_text);
|
||||
if extra_text.contains("+FHNG:") {
|
||||
return Err(FaxError::protocol(format!("Fax hangup: {}", extra_text.trim())));
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Some modems send +FCON without CONNECT
|
||||
if text.contains("+FCON") {
|
||||
tracing::info!("Fax conference established");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// Class 2: wait for fax session indicators
|
||||
// CONNECT indicates fax session is ready
|
||||
// The modem returns: CONNECT, +FCON: <params>
|
||||
let extra = self.driver.read_all(5000)?;
|
||||
let extra_text = String::from_utf8_lossy(&extra);
|
||||
tracing::info!("Session info: {}", extra_text);
|
||||
|
||||
Ok(())
|
||||
Err(FaxError::Timeout { ms: 60000 })
|
||||
}
|
||||
|
||||
fn send_page(&mut self, page: &Page) -> Result<()> {
|
||||
@@ -120,13 +167,29 @@ impl<'a> Class2Send<'a> {
|
||||
self.driver.write_raw(b"AT+FDT\r")?;
|
||||
self.driver.flush()?;
|
||||
|
||||
// Wait for CONNECT
|
||||
let response = self.driver.read_until(b"CONNECT\r\n", 10000)?;
|
||||
let text = String::from_utf8_lossy(&response);
|
||||
tracing::debug!("FDT response: {}", text);
|
||||
// Wait for CONNECT with longer timeout and retry logic
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
|
||||
let mut got_connect = false;
|
||||
while std::time::Instant::now() < deadline {
|
||||
let response = match self.driver.read_until(b"\r\n", 5000) {
|
||||
Ok(r) => r,
|
||||
Err(crate::error::FaxError::Timeout { .. }) => continue,
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
let text = String::from_utf8_lossy(&response);
|
||||
tracing::debug!("FDT response: {}", text);
|
||||
|
||||
if text.contains("ERROR") {
|
||||
return Err(FaxError::modem("FDT command failed"));
|
||||
if text.contains("CONNECT") {
|
||||
got_connect = true;
|
||||
break;
|
||||
}
|
||||
if text.contains("ERROR") {
|
||||
return Err(FaxError::modem("FDT command failed"));
|
||||
}
|
||||
}
|
||||
|
||||
if !got_connect {
|
||||
return Err(FaxError::Timeout { ms: 30000 });
|
||||
}
|
||||
|
||||
// Encode pixel data to T.4 MH format
|
||||
@@ -163,6 +226,10 @@ impl<'a> Class2Send<'a> {
|
||||
// Wait for confirmation
|
||||
let response = self.driver.read_until(b"\r\n", 10000)?;
|
||||
tracing::debug!("End page response: {}", String::from_utf8_lossy(&response));
|
||||
|
||||
// Brief pause before next page
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
self.driver.drain()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user