Add PDF to fax conversion support

- pdf.rs: Ghostscript-based PDF to TIFF-F conversion
- tiff_to_fax_document: Parse TIFF and convert to 1-bit fax format
- CLI: Add 'convert' subcommand for dry testing
- CLI: Add resolution option for send command
- Test files for PDF/TIFF conversion validation
This commit is contained in:
Warren
2026-07-01 17:17:49 +08:00
parent 13438289fe
commit bf1ca90a9c
11 changed files with 361 additions and 6 deletions
+98
View File
@@ -0,0 +1,98 @@
use crate::error::{FaxError, Result};
use crate::document::convert::{DocumentFormat, FaxDocument, Page};
use crate::config::FaxResolution;
use std::path::Path;
use std::process::Command;
/// Convert PDF to FaxDocument using Ghostscript.
pub fn pdf_to_fax_document(pdf_path: &Path, resolution: FaxResolution) -> Result<FaxDocument> {
let dpi_arg = match resolution {
FaxResolution::Standard => "204x98",
FaxResolution::Fine => "204x196",
FaxResolution::SuperFine => "204x392",
};
let tiff_path = pdf_path.with_extension("fax.tif");
tracing::info!("Converting PDF {} to TIFF {}", pdf_path.display(), tiff_path.display());
let output = Command::new("gs")
.arg("-dBATCH")
.arg("-dNOPAUSE")
.arg("-q")
.arg("-sDEVICE=tiffgray")
.arg(format!("-r{}", dpi_arg))
.arg(format!("-sOutputFile={}", tiff_path.display()))
.arg(pdf_path)
.output()
.map_err(|e| FaxError::document(format!("Failed to run ghostscript: {}", e)))?;
tracing::info!("Ghostscript exit status: {:?}", output.status);
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
tracing::error!("Ghostscript stderr: {}", stderr);
tracing::error!("Ghostscript stdout: {}", stdout);
return Err(FaxError::document(format!(
"Ghostscript failed: exit code {:?}",
output.status.code()
)));
}
let tiff_data = std::fs::read(&tiff_path)?;
let doc = tiff_to_fax_document(&tiff_data)?;
std::fs::remove_file(&tiff_path).ok();
Ok(doc)
}
/// Read TIFF-F and convert to FaxDocument with 1-bit pixel data.
pub fn tiff_to_fax_document(tiff_data: &[u8]) -> Result<FaxDocument> {
use tiff::decoder::Decoder;
use std::io::Cursor;
let mut cursor = Cursor::new(tiff_data);
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 bytes_per_row = ((width + 7) / 8) as usize;
let mut pixels = vec![0u8; bytes_per_row * height as usize];
let mut result = decoder.read_image()
.map_err(|e| FaxError::document(format!("Failed to read TIFF image: {}", e)))?;
// Convert to 1-bit packed format
let buffer = result.as_buffer(0);
let img_data = buffer.as_bytes();
// 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;
}
}
}
let page = Page {
pixels,
width_pels: width,
rows: height,
};
Ok(FaxDocument {
pages: vec![page],
format: DocumentFormat::Tiff,
})
}