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 { 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 { 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 mut pages = Vec::new(); loop { let (width, height) = decoder.dimensions() .map_err(|e| FaxError::document(format!("Failed to get TIFF dimensions: {}", e)))?; 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() .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; } } } // 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, rows: height, }); // Try to advance to next page match decoder.next_image() { Ok(_) => continue, Err(_) => break, } } if pages.is_empty() { return Err(FaxError::document("TIFF contains no pages")); } Ok(FaxDocument { pages, format: DocumentFormat::Tiff, }) }