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:
Warren
2026-07-02 18:43:16 +08:00
parent b102917644
commit d1e92b32fb
20 changed files with 1849 additions and 83 deletions
+41 -27
View File
@@ -55,44 +55,58 @@ pub fn tiff_to_fax_document(tiff_data: &[u8]) -> Result<FaxDocument> {
let mut decoder = Decoder::new(&mut cursor)
.map_err(|e| FaxError::document(format!("Failed to open TIFF: {}", e)))?;
let (width, height) = decoder.dimensions()
.map_err(|e| FaxError::document(format!("Failed to get TIFF dimensions: {}", e)))?;
let mut pages = Vec::new();
let bytes_per_row = ((width + 7) / 8) as usize;
let mut pixels = vec![0u8; bytes_per_row * height as usize];
loop {
let (width, height) = decoder.dimensions()
.map_err(|e| FaxError::document(format!("Failed to get TIFF dimensions: {}", e)))?;
let mut result = decoder.read_image()
.map_err(|e| FaxError::document(format!("Failed to read TIFF image: {}", e)))?;
let bytes_per_row = ((width + 7) / 8) as usize;
let mut pixels = vec![0u8; bytes_per_row * height as usize];
// Convert to 1-bit packed format
let buffer = result.as_buffer(0);
let img_data = buffer.as_bytes();
let mut result = decoder.read_image()
.map_err(|e| FaxError::document(format!("Failed to read TIFF image: {}", e)))?;
// Process each pixel - TIFF is grayscale (8-bit), convert to 1-bit
for (i, &byte) in img_data.iter().enumerate() {
let pixel_idx = i % width as usize;
let row_idx = i / width as usize;
let byte_idx = pixel_idx / 8;
let bit_idx = 7 - (pixel_idx % 8);
// Convert to 1-bit packed format
let buffer = result.as_buffer(0);
let img_data = buffer.as_bytes();
// WhiteIsZero: 0=white, 255=black
// We want: 1=black, 0=white (MSB first)
if byte < 128 {
let idx = row_idx * bytes_per_row + byte_idx;
if idx < pixels.len() {
pixels[idx] |= 1 << bit_idx;
// Process each pixel - TIFF is grayscale (8-bit), convert to 1-bit
for (i, &byte) in img_data.iter().enumerate() {
let pixel_idx = i % width as usize;
let row_idx = i / width as usize;
let byte_idx = pixel_idx / 8;
let bit_idx = 7 - (pixel_idx % 8);
// WhiteIsZero: 0=white, 255=black
// We want: 1=black, 0=white (MSB first)
if byte < 128 {
let idx = row_idx * bytes_per_row + byte_idx;
if idx < pixels.len() {
pixels[idx] |= 1 << bit_idx;
}
}
}
pages.push(Page {
pixels,
width_pels: width,
rows: height,
});
// Try to advance to next page
match decoder.next_image() {
Ok(_) => continue,
Err(_) => break,
}
}
let page = Page {
pixels,
width_pels: width,
rows: height,
};
if pages.is_empty() {
return Err(FaxError::document("TIFF contains no pages"));
}
Ok(FaxDocument {
pages: vec![page],
pages,
format: DocumentFormat::Tiff,
})
}