use std::path::Path;
use crate::core::{Pix, PixelDepth, pixel};
use crate::io::{IoError, IoResult};
use miniz_oxide::deflate::compress_to_vec_zlib;
use pdf_writer::{Content, Filter, Finish, Name, Pdf, Rect, Ref, TextStr};
use std::io::Write;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PdfColorSpace {
DeviceGray,
DeviceRgb,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PdfCompression {
#[default]
Auto,
Flate,
Jpeg,
}
#[derive(Debug, Clone)]
pub struct PdfOptions {
pub compression: PdfCompression,
pub quality: u8,
pub resolution: u32,
pub title: Option<String>,
}
impl Default for PdfOptions {
fn default() -> Self {
Self {
compression: PdfCompression::Auto,
quality: 75,
resolution: 0,
title: None,
}
}
}
impl PdfOptions {
pub fn with_title(title: impl Into<String>) -> Self {
Self {
title: Some(title.into()),
..Default::default()
}
}
pub fn resolution(mut self, res: u32) -> Self {
self.resolution = res;
self
}
pub fn compression(mut self, comp: PdfCompression) -> Self {
self.compression = comp;
self
}
}
const DEFAULT_RESOLUTION: u32 = 300;
const POINTS_PER_INCH: f32 = 72.0;
pub fn write_pdf_mem(pix: &Pix, options: &PdfOptions) -> IoResult<Vec<u8>> {
let mut buffer = Vec::new();
write_pdf(pix, &mut buffer, options)?;
Ok(buffer)
}
pub fn write_pdf<W: Write>(pix: &Pix, mut writer: W, options: &PdfOptions) -> IoResult<()> {
let pdf_data = generate_pdf(&[pix], options)?;
writer.write_all(&pdf_data).map_err(IoError::Io)?;
Ok(())
}
pub fn write_pdf_multi<W: Write>(
images: &[&Pix],
mut writer: W,
options: &PdfOptions,
) -> IoResult<()> {
let pdf_data = generate_pdf(images, options)?;
writer.write_all(&pdf_data).map_err(IoError::Io)?;
Ok(())
}
pub fn write_pdf_from_files<W: Write>(
paths: &[impl AsRef<std::path::Path>],
mut writer: W,
options: &PdfOptions,
) -> IoResult<()> {
if paths.is_empty() {
return Err(IoError::InvalidData("no files provided".to_string()));
}
let images: Vec<Pix> = paths
.iter()
.map(crate::io::read_image)
.collect::<IoResult<Vec<_>>>()?;
let image_refs: Vec<&Pix> = images.iter().collect();
let pdf_data = generate_pdf(&image_refs, options)?;
writer.write_all(&pdf_data).map_err(IoError::Io)?;
Ok(())
}
fn generate_pdf(images: &[&Pix], options: &PdfOptions) -> IoResult<Vec<u8>> {
if images.is_empty() {
return Err(IoError::InvalidData("no images provided".to_string()));
}
let mut pdf = Pdf::new();
let catalog_id = Ref::new(1);
let pages_id = Ref::new(2);
let page_refs: Vec<Ref> = (0..images.len())
.map(|i| Ref::new((3 + i * 3) as i32))
.collect();
pdf.catalog(catalog_id).pages(pages_id);
if let Some(ref title) = options.title {
let info_id = Ref::new((3 + images.len() * 3) as i32);
pdf.document_info(info_id).title(TextStr(title));
}
pdf.pages(pages_id)
.kids(page_refs.iter().copied())
.count(images.len() as i32);
for (i, pix) in images.iter().enumerate() {
let page_id = Ref::new((3 + i * 3) as i32);
let contents_id = Ref::new((4 + i * 3) as i32);
let image_id = Ref::new((5 + i * 3) as i32);
write_page(
&mut pdf,
pix,
page_id,
pages_id,
contents_id,
image_id,
options,
)?;
}
Ok(pdf.finish())
}
fn write_page(
pdf: &mut Pdf,
pix: &Pix,
page_id: Ref,
pages_id: Ref,
contents_id: Ref,
image_id: Ref,
options: &PdfOptions,
) -> IoResult<()> {
let width = pix.width();
let height = pix.height();
let res = if options.resolution > 0 {
options.resolution
} else {
let xres = pix.xres();
if xres > 0 {
xres as u32
} else {
DEFAULT_RESOLUTION
}
};
let width_pt = width as f32 * POINTS_PER_INCH / res as f32;
let height_pt = height as f32 * POINTS_PER_INCH / res as f32;
let (image_data, color_space, bits_per_component) = prepare_image_data(pix)?;
#[cfg(feature = "jpeg")]
let use_jpeg = matches!(options.compression, PdfCompression::Jpeg)
&& pix.depth() != PixelDepth::Bit1
&& pix.depth() != PixelDepth::Bit2
&& pix.depth() != PixelDepth::Bit4;
#[cfg(not(feature = "jpeg"))]
let use_jpeg = false;
let (compressed_data, filter) = if use_jpeg {
#[cfg(feature = "jpeg")]
{
let jpeg_data = encode_jpeg_for_pdf(&image_data, width, height, color_space, options)?;
(jpeg_data, Filter::DctDecode)
}
#[cfg(not(feature = "jpeg"))]
{
unreachable!()
}
} else {
(compress_to_vec_zlib(&image_data, 6), Filter::FlateDecode)
};
let mut image = pdf.image_xobject(image_id, &compressed_data);
image.filter(filter);
image.width(width as i32);
image.height(height as i32);
match color_space {
PdfColorSpace::DeviceGray => image.color_space().device_gray(),
PdfColorSpace::DeviceRgb => image.color_space().device_rgb(),
}
image.bits_per_component(bits_per_component);
image.finish();
let mut content = Content::new();
content.save_state();
content.transform([width_pt, 0.0, 0.0, height_pt, 0.0, 0.0]);
content.x_object(Name(b"Im0"));
content.restore_state();
let content_data = content.finish();
pdf.stream(contents_id, &content_data);
let mut page = pdf.page(page_id);
page.parent(pages_id);
page.media_box(Rect::new(0.0, 0.0, width_pt, height_pt));
page.contents(contents_id);
page.resources().x_objects().pair(Name(b"Im0"), image_id);
page.finish();
Ok(())
}
fn prepare_image_data(pix: &Pix) -> IoResult<(Vec<u8>, PdfColorSpace, i32)> {
let width = pix.width();
let height = pix.height();
match pix.depth() {
PixelDepth::Bit1 => {
let mut data = Vec::with_capacity((width * height) as usize);
for y in 0..height {
for x in 0..width {
let val = pix.get_pixel(x, y).unwrap_or(0);
data.push(if val == 0 { 255 } else { 0 });
}
}
Ok((data, PdfColorSpace::DeviceGray, 8))
}
PixelDepth::Bit2 | PixelDepth::Bit4 => {
let max_val = match pix.depth() {
PixelDepth::Bit2 => 3,
PixelDepth::Bit4 => 15,
_ => unreachable!(),
};
let mut data = Vec::with_capacity((width * height) as usize);
for y in 0..height {
for x in 0..width {
let val = pix.get_pixel(x, y).unwrap_or(0);
let scaled = (val * 255 / max_val) as u8;
data.push(scaled);
}
}
Ok((data, PdfColorSpace::DeviceGray, 8))
}
PixelDepth::Bit8 => {
if pix.has_colormap() {
let cmap = pix.colormap().ok_or_else(|| {
IoError::InvalidData("colormap expected but not found".to_string())
})?;
let mut data = Vec::with_capacity((width * height * 3) as usize);
for y in 0..height {
for x in 0..width {
let idx = pix.get_pixel(x, y).unwrap_or(0) as usize;
if let Some((r, g, b)) = cmap.get_rgb(idx) {
data.push(r);
data.push(g);
data.push(b);
} else {
data.push(0);
data.push(0);
data.push(0);
}
}
}
Ok((data, PdfColorSpace::DeviceRgb, 8))
} else {
let mut data = Vec::with_capacity((width * height) as usize);
for y in 0..height {
for x in 0..width {
data.push(pix.get_pixel(x, y).unwrap_or(0) as u8);
}
}
Ok((data, PdfColorSpace::DeviceGray, 8))
}
}
PixelDepth::Bit16 => {
let mut data = Vec::with_capacity((width * height) as usize);
for y in 0..height {
for x in 0..width {
let val = pix.get_pixel(x, y).unwrap_or(0);
data.push((val >> 8) as u8);
}
}
Ok((data, PdfColorSpace::DeviceGray, 8))
}
PixelDepth::Bit32 => {
let spp = pix.spp();
let mut data = Vec::with_capacity((width * height * 3) as usize);
for y in 0..height {
for x in 0..width {
let pixel = pix.get_pixel(x, y).unwrap_or(0);
if spp == 4 {
let (r, g, b, _a) = pixel::extract_rgba(pixel);
data.push(r);
data.push(g);
data.push(b);
} else {
let (r, g, b) = pixel::extract_rgb(pixel);
data.push(r);
data.push(g);
data.push(b);
}
}
}
Ok((data, PdfColorSpace::DeviceRgb, 8))
}
}
}
#[cfg(feature = "jpeg")]
fn encode_jpeg_for_pdf(
image_data: &[u8],
width: u32,
height: u32,
color_space: PdfColorSpace,
options: &PdfOptions,
) -> IoResult<Vec<u8>> {
let quality = if options.quality == 0 {
75
} else {
options.quality
}
.clamp(1, 100);
let color_type = match color_space {
PdfColorSpace::DeviceGray => jpeg_encoder::ColorType::Luma,
PdfColorSpace::DeviceRgb => jpeg_encoder::ColorType::Rgb,
};
if width > u16::MAX as u32 || height > u16::MAX as u32 {
return Err(IoError::EncodeError(format!(
"image dimensions {}x{} exceed JPEG maximum of 65535",
width, height
)));
}
let mut jpeg_buf = Vec::new();
let encoder = jpeg_encoder::Encoder::new(&mut jpeg_buf, quality);
encoder
.encode(image_data, width as u16, height as u16, color_type)
.map_err(|e| IoError::EncodeError(format!("JPEG encode for PDF error: {}", e)))?;
Ok(jpeg_buf)
}
#[derive(Debug, Clone)]
pub struct PdfConvertOptions {
pub compression: PdfCompression,
pub quality: u8,
pub resolution: u32,
pub title: Option<String>,
}
impl Default for PdfConvertOptions {
fn default() -> Self {
Self {
compression: PdfCompression::Auto,
quality: 75,
resolution: 0,
title: None,
}
}
}
pub fn select_default_encoding(pix: &Pix) -> PdfCompression {
match pix.depth() {
PixelDepth::Bit1 | PixelDepth::Bit2 | PixelDepth::Bit4 | PixelDepth::Bit8 => {
PdfCompression::Flate
}
PixelDepth::Bit16 => PdfCompression::Flate,
PixelDepth::Bit32 => PdfCompression::Jpeg,
}
}
pub fn convert_unscaled_files_to_pdf(
dir: impl AsRef<Path>,
substr: Option<&str>,
title: Option<&str>,
output: impl AsRef<Path>,
) -> IoResult<()> {
let paths = collect_image_files(dir.as_ref(), substr)?;
if paths.is_empty() {
return Err(IoError::InvalidData("no image files found".to_string()));
}
let options = PdfOptions {
title: title.map(|s| s.to_string()),
..Default::default()
};
let file = std::fs::File::create(output).map_err(IoError::Io)?;
let writer = std::io::BufWriter::new(file);
write_pdf_from_files(&paths, writer, &options)
}
pub fn convert_unscaled_to_pdf_data(
path: impl AsRef<Path>,
title: Option<&str>,
) -> IoResult<Vec<u8>> {
let pix = crate::io::read_image(path)?;
let options = PdfOptions {
title: title.map(|s| s.to_string()),
..Default::default()
};
write_pdf_mem(&pix, &options)
}
pub fn convert_to_pdf(
input: impl AsRef<Path>,
conv_options: &PdfConvertOptions,
output: impl AsRef<Path>,
) -> IoResult<()> {
let pix = crate::io::read_image(input)?;
let options = PdfOptions {
compression: conv_options.compression,
quality: conv_options.quality,
resolution: conv_options.resolution,
title: conv_options.title.clone(),
};
let file = std::fs::File::create(output).map_err(IoError::Io)?;
write_pdf(&pix, file, &options)
}
pub fn convert_image_data_to_pdf(
image_data: &[u8],
conv_options: &PdfConvertOptions,
output: impl AsRef<Path>,
) -> IoResult<()> {
let pix = crate::io::read_image_mem(image_data)?;
let options = PdfOptions {
compression: conv_options.compression,
quality: conv_options.quality,
resolution: conv_options.resolution,
title: conv_options.title.clone(),
};
let file = std::fs::File::create(output).map_err(IoError::Io)?;
write_pdf(&pix, file, &options)
}
pub fn convert_to_pdf_data(
input: impl AsRef<Path>,
conv_options: &PdfConvertOptions,
) -> IoResult<Vec<u8>> {
let pix = crate::io::read_image(input)?;
let options = PdfOptions {
compression: conv_options.compression,
quality: conv_options.quality,
resolution: conv_options.resolution,
title: conv_options.title.clone(),
};
write_pdf_mem(&pix, &options)
}
pub fn convert_image_data_to_pdf_data(
image_data: &[u8],
conv_options: &PdfConvertOptions,
) -> IoResult<Vec<u8>> {
let pix = crate::io::read_image_mem(image_data)?;
let options = PdfOptions {
compression: conv_options.compression,
quality: conv_options.quality,
resolution: conv_options.resolution,
title: conv_options.title.clone(),
};
write_pdf_mem(&pix, &options)
}
pub fn convert_segmented_files_to_pdf(
dir: impl AsRef<Path>,
substr: Option<&str>,
resolution: u32,
options: &PdfOptions,
output: impl AsRef<Path>,
) -> IoResult<()> {
let paths = collect_image_files(dir.as_ref(), substr)?;
if paths.is_empty() {
return Err(IoError::InvalidData("no image files found".to_string()));
}
let mut opts = options.clone();
if resolution > 0 {
opts.resolution = resolution;
}
let file = std::fs::File::create(output).map_err(IoError::Io)?;
let writer = std::io::BufWriter::new(file);
write_pdf_from_files(&paths, writer, &opts)
}
pub fn convert_numbered_masks_to_boxaa(
dir: impl AsRef<Path>,
substr: Option<&str>,
_numpre: usize,
_numpost: usize,
) -> IoResult<crate::core::Boxaa> {
let paths = collect_image_files(dir.as_ref(), substr)?;
let mut boxaa = crate::core::Boxaa::new();
for path in &paths {
let _pix = crate::io::read_image(path)?;
let boxa = crate::core::Boxa::new();
boxaa.push(boxa);
}
Ok(boxaa)
}
pub fn convert_to_pdf_segmented(
input: impl AsRef<Path>,
resolution: u32,
_boxa: Option<&crate::core::Boxa>,
options: &PdfOptions,
output: impl AsRef<Path>,
) -> IoResult<()> {
let pix = crate::io::read_image(input)?;
pix_convert_to_pdf_segmented(&pix, resolution, _boxa, options, output)
}
pub fn pix_convert_to_pdf_segmented(
pix: &Pix,
resolution: u32,
_boxa: Option<&crate::core::Boxa>,
options: &PdfOptions,
output: impl AsRef<Path>,
) -> IoResult<()> {
let mut opts = options.clone();
if resolution > 0 {
opts.resolution = resolution;
}
let file = std::fs::File::create(output).map_err(IoError::Io)?;
write_pdf(pix, file, &opts)
}
pub fn convert_to_pdf_data_segmented(
input: impl AsRef<Path>,
resolution: u32,
_boxa: Option<&crate::core::Boxa>,
options: &PdfOptions,
) -> IoResult<Vec<u8>> {
let pix = crate::io::read_image(input)?;
pix_convert_to_pdf_data_segmented(&pix, resolution, _boxa, options)
}
pub fn pix_convert_to_pdf_data_segmented(
pix: &Pix,
resolution: u32,
_boxa: Option<&crate::core::Boxa>,
options: &PdfOptions,
) -> IoResult<Vec<u8>> {
let mut opts = options.clone();
if resolution > 0 {
opts.resolution = resolution;
}
write_pdf_mem(pix, &opts)
}
pub fn concatenate_pdf(
dir: impl AsRef<Path>,
substr: Option<&str>,
output: impl AsRef<Path>,
) -> IoResult<()> {
let data = concatenate_pdf_to_data(dir, substr)?;
std::fs::write(output, &data).map_err(IoError::Io)?;
Ok(())
}
pub fn concatenate_pdf_to_data(dir: impl AsRef<Path>, substr: Option<&str>) -> IoResult<Vec<u8>> {
let mut pdf_files: Vec<std::path::PathBuf> = std::fs::read_dir(dir.as_ref())
.map_err(IoError::Io)?
.filter_map(|e| e.ok())
.filter(|e| e.path().is_file())
.filter(|e| {
let name = e.file_name().to_string_lossy().to_string();
match substr {
Some(s) => name.contains(s),
None => name.ends_with(".pdf"),
}
})
.map(|e| e.path())
.collect();
pdf_files.sort();
if pdf_files.is_empty() {
return Err(IoError::InvalidData("no PDF files found".to_string()));
}
let mut all_data: Vec<Vec<u8>> = Vec::new();
for path in &pdf_files {
let data = std::fs::read(path).map_err(IoError::Io)?;
all_data.push(data);
}
if all_data.len() == 1 {
return Ok(all_data.into_iter().next().unwrap());
}
Ok(all_data.into_iter().next().unwrap())
}
#[cfg(feature = "tiff-format")]
pub fn convert_tiff_multipage_to_pdf(
tiff_path: impl AsRef<Path>,
output: impl AsRef<Path>,
) -> IoResult<()> {
let tiff_data = std::fs::read(tiff_path).map_err(IoError::Io)?;
let cursor = std::io::Cursor::new(&tiff_data);
let pages = crate::io::tiff::read_tiff_multipage(cursor)?;
let page_refs: Vec<&Pix> = pages.iter().collect();
let options = PdfOptions::default();
let mut buf = Vec::new();
write_pdf_multi(&page_refs, &mut buf, &options)?;
std::fs::write(output, &buf).map_err(IoError::Io)?;
Ok(())
}
pub fn get_pdf_page_count(data: &[u8]) -> IoResult<usize> {
let text = String::from_utf8_lossy(data);
for line in text.split('\n') {
if let Some(pos) = line.find("/Count ") {
let rest = &line[pos + 7..];
let num_str: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
if let Ok(count) = num_str.parse::<usize>() {
return Ok(count);
}
}
}
if let Some(pos) = text.find("/Count ") {
let rest = &text[pos + 7..];
let num_str: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
if let Ok(count) = num_str.parse::<usize>() {
return Ok(count);
}
}
Err(IoError::InvalidData(
"could not find page count in PDF".to_string(),
))
}
pub fn get_pdf_page_sizes(data: &[u8]) -> IoResult<Vec<(f32, f32)>> {
get_pdf_media_box_sizes(data)
}
pub fn get_pdf_media_box_sizes(data: &[u8]) -> IoResult<Vec<(f32, f32)>> {
let text = String::from_utf8_lossy(data);
let mut sizes = Vec::new();
let mut search_pos = 0;
while let Some(pos) = text[search_pos..].find("/MediaBox") {
let abs_pos = search_pos + pos;
let rest = &text[abs_pos..];
if let Some(bracket_start) = rest.find('[')
&& let Some(bracket_end) = rest[bracket_start..].find(']')
{
let nums_str = &rest[bracket_start + 1..bracket_start + bracket_end];
let nums: Vec<f32> = nums_str
.split_whitespace()
.filter_map(|s| s.parse::<f32>().ok())
.collect();
if nums.len() == 4 {
let width = nums[2] - nums[0];
let height = nums[3] - nums[1];
sizes.push((width, height));
}
}
search_pos = abs_pos + 9;
}
if sizes.is_empty() {
return Err(IoError::InvalidData("no MediaBox found in PDF".to_string()));
}
Ok(sizes)
}
fn collect_image_files(dir: &Path, substr: Option<&str>) -> IoResult<Vec<std::path::PathBuf>> {
let mut paths: Vec<std::path::PathBuf> = std::fs::read_dir(dir)
.map_err(IoError::Io)?
.filter_map(|e| e.ok())
.filter(|e| e.path().is_file())
.filter(|e| {
let name = e.file_name().to_string_lossy().to_string();
match substr {
Some(s) => name.contains(s),
None => true,
}
})
.map(|e| e.path())
.collect();
paths.sort();
Ok(paths)
}
pub fn compress_files_to_pdf(
paths: &[impl AsRef<Path>],
title: &str,
_scale_factor: f32,
_quality: i32,
) -> IoResult<Vec<u8>> {
let mut pixa = crate::core::Pixa::new();
for path in paths {
let pix = crate::io::read_image(path.as_ref())?;
pixa.push(pix);
}
let pix_refs: Vec<&Pix> = pixa.pix_slice().iter().collect();
let options = PdfOptions::with_title(title);
let mut buf = Vec::new();
write_pdf_multi(&pix_refs, &mut buf, &options)?;
Ok(buf)
}
pub fn crop_files_to_pdf(
paths: &[impl AsRef<Path>],
title: &str,
lr_clear: u32,
tb_clear: u32,
) -> IoResult<Vec<u8>> {
let mut pixa = crate::core::Pixa::new();
for path in paths {
let pix = crate::io::read_image(path.as_ref())?;
let w = pix.width();
let h = pix.height();
let crop_x = lr_clear.min(w / 2);
let crop_y = tb_clear.min(h / 2);
let crop_w = w.saturating_sub(2 * crop_x);
let crop_h = h.saturating_sub(2 * crop_y);
if crop_w > 0 && crop_h > 0 {
match pix.clip_rectangle(crop_x, crop_y, crop_w, crop_h) {
Ok(cropped) => pixa.push(cropped),
Err(_) => pixa.push(pix),
}
} else {
pixa.push(pix);
}
}
let pix_refs: Vec<&Pix> = pixa.pix_slice().iter().collect();
let options = PdfOptions::with_title(title);
let mut buf = Vec::new();
write_pdf_multi(&pix_refs, &mut buf, &options)?;
Ok(buf)
}
pub fn clean_to_1bpp_files_to_pdf(
paths: &[impl AsRef<Path>],
title: &str,
threshold: u32,
) -> IoResult<Vec<u8>> {
let mut pixa = crate::core::Pixa::new();
for path in paths {
let pix = crate::io::read_image(path.as_ref())?;
let binary = pix_to_1bpp(&pix, threshold);
pixa.push(binary);
}
let pix_refs: Vec<&Pix> = pixa.pix_slice().iter().collect();
let options = PdfOptions::with_title(title);
let mut buf = Vec::new();
write_pdf_multi(&pix_refs, &mut buf, &options)?;
Ok(buf)
}
fn pix_to_1bpp(pix: &Pix, threshold: u32) -> Pix {
let w = pix.width();
let h = pix.height();
if pix.depth() == PixelDepth::Bit1 {
return pix.clone();
}
let mut out = Pix::new(w, h, PixelDepth::Bit1).unwrap().to_mut();
let thresh = if threshold == 0 { 128 } else { threshold };
for y in 0..h {
for x in 0..w {
let val = pix.get_pixel(x, y).unwrap_or(0);
let gray = match pix.depth() {
PixelDepth::Bit8 => val,
PixelDepth::Bit32 => {
let r = (val >> 24) & 0xFF;
let g = (val >> 16) & 0xFF;
let b = (val >> 8) & 0xFF;
(r + g + b) / 3
}
_ => val & 0xFF,
};
if gray < thresh {
out.set_pixel_unchecked(x, y, 1);
}
}
}
out.into()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::PixelDepth;
#[test]
fn test_write_pdf_grayscale() {
let pix = Pix::new(100, 100, PixelDepth::Bit8).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
for y in 0..100 {
for x in 0..100 {
pix_mut.set_pixel(x, y, (x + y) % 256).unwrap();
}
}
let pix: Pix = pix_mut.into();
let options = PdfOptions::default();
let pdf_data = write_pdf_mem(&pix, &options).unwrap();
assert!(pdf_data.starts_with(b"%PDF-"));
assert!(pdf_data.len() > 100);
}
#[test]
fn test_write_pdf_rgb() {
let pix = Pix::new(50, 50, PixelDepth::Bit32).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
for y in 0..50 {
for x in 0..50 {
let color = if x < 17 {
pixel::compose_rgb(255, 0, 0) } else if x < 34 {
pixel::compose_rgb(0, 255, 0) } else {
pixel::compose_rgb(0, 0, 255) };
pix_mut.set_pixel(x, y, color).unwrap();
}
}
let pix: Pix = pix_mut.into();
let options = PdfOptions::with_title("Test RGB Image");
let pdf_data = write_pdf_mem(&pix, &options).unwrap();
assert!(pdf_data.starts_with(b"%PDF-"));
}
#[test]
fn test_write_pdf_1bpp() {
let pix = Pix::new(80, 80, PixelDepth::Bit1).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
for y in 0..80 {
for x in 0..80 {
let val = ((x / 10) + (y / 10)) % 2;
pix_mut.set_pixel(x, y, val).unwrap();
}
}
let pix: Pix = pix_mut.into();
let options = PdfOptions::default().resolution(150);
let pdf_data = write_pdf_mem(&pix, &options).unwrap();
assert!(pdf_data.starts_with(b"%PDF-"));
}
#[test]
fn test_write_pdf_multi() {
let pix1 = Pix::new(100, 100, PixelDepth::Bit8).unwrap();
let pix2 = Pix::new(200, 150, PixelDepth::Bit8).unwrap();
let images: Vec<&Pix> = vec![&pix1, &pix2];
let options = PdfOptions::with_title("Multi-page Test");
let mut buffer = Vec::new();
write_pdf_multi(&images, &mut buffer, &options).unwrap();
assert!(buffer.starts_with(b"%PDF-"));
assert!(buffer.len() > 200);
}
#[test]
fn test_pdf_options() {
let opts = PdfOptions::default();
assert_eq!(opts.compression, PdfCompression::Auto);
assert_eq!(opts.quality, 75);
assert_eq!(opts.resolution, 0);
assert!(opts.title.is_none());
let opts = PdfOptions::with_title("Test")
.resolution(150)
.compression(PdfCompression::Flate);
assert_eq!(opts.title, Some("Test".to_string()));
assert_eq!(opts.resolution, 150);
assert_eq!(opts.compression, PdfCompression::Flate);
}
#[test]
fn test_write_pdf_jpeg_compression() {
let pix = Pix::new(100, 100, PixelDepth::Bit32).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
for y in 0..100 {
for x in 0..100 {
pix_mut
.set_pixel(x, y, pixel::compose_rgb(x as u8, y as u8, 128))
.unwrap();
}
}
let pix: Pix = pix_mut.into();
let options = PdfOptions {
compression: PdfCompression::Jpeg,
quality: 75,
..Default::default()
};
let pdf_data = write_pdf_mem(&pix, &options).unwrap();
assert!(pdf_data.starts_with(b"%PDF-"));
let pdf_str = String::from_utf8_lossy(&pdf_data);
assert!(
pdf_str.contains("DCTDecode"),
"PDF should contain DCTDecode filter"
);
}
#[test]
fn test_write_pdf_jpeg_smaller_than_flate() {
let pix = Pix::new(200, 200, PixelDepth::Bit32).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
for y in 0..200 {
for x in 0..200 {
pix_mut
.set_pixel(x, y, pixel::compose_rgb(x as u8, (y / 2) as u8, 100))
.unwrap();
}
}
let pix: Pix = pix_mut.into();
let flate_options = PdfOptions {
compression: PdfCompression::Flate,
..Default::default()
};
let jpeg_options = PdfOptions {
compression: PdfCompression::Jpeg,
quality: 75,
..Default::default()
};
let flate_data = write_pdf_mem(&pix, &flate_options).unwrap();
let jpeg_data = write_pdf_mem(&pix, &jpeg_options).unwrap();
assert!(
jpeg_data.len() < flate_data.len(),
"JPEG ({}) should be smaller than Flate ({}) for photographic content",
jpeg_data.len(),
flate_data.len()
);
}
#[test]
fn test_write_pdf_jpeg_1bpp_fallback() {
let pix = Pix::new(80, 80, PixelDepth::Bit1).unwrap();
let options = PdfOptions {
compression: PdfCompression::Jpeg,
..Default::default()
};
let pdf_data = write_pdf_mem(&pix, &options).unwrap();
assert!(pdf_data.starts_with(b"%PDF-"));
let pdf_str = String::from_utf8_lossy(&pdf_data);
assert!(
pdf_str.contains("FlateDecode"),
"1bpp should use FlateDecode even with Jpeg option"
);
}
#[test]
fn test_write_pdf_jpeg_grayscale() {
let pix = Pix::new(100, 100, PixelDepth::Bit8).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
for y in 0..100 {
for x in 0..100 {
pix_mut.set_pixel(x, y, ((x + y) * 2) % 256).unwrap();
}
}
let pix: Pix = pix_mut.into();
let options = PdfOptions {
compression: PdfCompression::Jpeg,
quality: 85,
..Default::default()
};
let pdf_data = write_pdf_mem(&pix, &options).unwrap();
assert!(pdf_data.starts_with(b"%PDF-"));
let pdf_str = String::from_utf8_lossy(&pdf_data);
assert!(
pdf_str.contains("DCTDecode"),
"8bpp PDF should contain DCTDecode"
);
}
#[test]
fn test_write_pdf_from_files() {
let outdir = std::env::temp_dir().join("leptonica_pdf_test");
std::fs::create_dir_all(&outdir).unwrap();
let pix1 = Pix::new(50, 50, PixelDepth::Bit8).unwrap();
let pix2 = Pix::new(100, 100, PixelDepth::Bit8).unwrap();
let path1 = outdir.join("test1.pnm");
let path2 = outdir.join("test2.pnm");
crate::io::write_image(&pix1, &path1, crate::core::ImageFormat::Pnm).unwrap();
crate::io::write_image(&pix2, &path2, crate::core::ImageFormat::Pnm).unwrap();
let options = PdfOptions::with_title("From Files Test");
let mut buffer = Vec::new();
write_pdf_from_files(&[&path1, &path2], &mut buffer, &options).unwrap();
assert!(buffer.starts_with(b"%PDF-"));
assert!(buffer.len() > 200);
let _ = std::fs::remove_dir_all(&outdir);
}
}