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:
@@ -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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user