mod ascii85;
use crate::core::{Pix, PixelDepth, pixel};
use crate::io::{IoError, IoResult};
use miniz_oxide::deflate::compress_to_vec_zlib;
use std::io::Write;
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PsLevel {
Level1,
Level2,
#[default]
Level3,
}
#[derive(Debug, Clone)]
pub struct PsOptions {
pub level: PsLevel,
pub resolution: u32,
pub quality: u8,
pub scale: f32,
pub write_bounding_box: bool,
pub title: Option<String>,
pub page_number: u32,
}
impl Default for PsOptions {
fn default() -> Self {
Self {
level: PsLevel::Level3,
resolution: 0,
quality: 75,
scale: 1.0,
write_bounding_box: true,
title: None,
page_number: 1,
}
}
}
impl PsOptions {
pub fn eps() -> Self {
Self {
write_bounding_box: true,
..Default::default()
}
}
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 level(mut self, level: PsLevel) -> Self {
self.level = level;
self
}
pub fn scale(mut self, scale: f32) -> Self {
self.scale = scale;
self
}
pub fn quality(mut self, quality: u8) -> Self {
self.quality = quality;
self
}
pub fn bounding_box(mut self, enable: bool) -> Self {
self.write_bounding_box = enable;
self
}
}
const DEFAULT_RESOLUTION: u32 = 300;
const POINTS_PER_INCH: f32 = 72.0;
const LETTER_WIDTH: f32 = 612.0;
const LETTER_HEIGHT: f32 = 792.0;
pub fn write_ps_mem(pix: &Pix, options: &PsOptions) -> IoResult<Vec<u8>> {
let mut buffer = Vec::new();
write_ps(pix, &mut buffer, options)?;
Ok(buffer)
}
pub fn write_ps<W: Write>(pix: &Pix, mut writer: W, options: &PsOptions) -> IoResult<()> {
let ps_string = generate_ps(pix, options)?;
writer
.write_all(ps_string.as_bytes())
.map_err(IoError::Io)?;
Ok(())
}
pub fn write_ps_multi<W: Write>(
images: &[&Pix],
mut writer: W,
options: &PsOptions,
) -> IoResult<()> {
if images.is_empty() {
return Err(IoError::InvalidData("no images provided".to_string()));
}
let mut ps = String::new();
ps.push_str("%!PS-Adobe-3.0\n");
ps.push_str("%%Creator: leptonica-rs\n");
if let Some(ref title) = options.title {
ps.push_str(&format!("%%Title: {}\n", title));
}
ps.push_str(&format!("%%Pages: {}\n", images.len()));
ps.push_str("%%EndComments\n");
writer.write_all(ps.as_bytes()).map_err(IoError::Io)?;
for (i, pix) in images.iter().enumerate() {
let mut page_options = options.clone();
page_options.page_number = (i + 1) as u32;
page_options.write_bounding_box = false;
let page_ps = generate_ps(pix, &page_options)?;
writer.write_all(page_ps.as_bytes()).map_err(IoError::Io)?;
}
writer.write_all(b"%%EOF\n").map_err(IoError::Io)?;
Ok(())
}
pub fn write_eps_mem(pix: &Pix, options: &PsOptions) -> IoResult<Vec<u8>> {
let mut eps_options = options.clone();
eps_options.write_bounding_box = true;
write_ps_mem(pix, &eps_options)
}
fn generate_ps(pix: &Pix, options: &PsOptions) -> IoResult<String> {
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 scale = if options.scale <= 0.0 {
1.0
} else {
options.scale
};
let effective_res = res as f32 / scale;
let width_pt = width as f32 * POINTS_PER_INCH / effective_res;
let height_pt = height as f32 * POINTS_PER_INCH / effective_res;
let x_pt = (LETTER_WIDTH - width_pt) / 2.0;
let y_pt = (LETTER_HEIGHT - height_pt) / 2.0;
match options.level {
PsLevel::Level1 => generate_uncompressed_ps(pix, options, x_pt, y_pt, width_pt, height_pt),
PsLevel::Level2 => {
if pix.depth() == PixelDepth::Bit1 {
generate_flate_ps(pix, options, x_pt, y_pt, width_pt, height_pt)
} else {
generate_dct_ps(pix, options, x_pt, y_pt, width_pt, height_pt)
}
}
PsLevel::Level3 => generate_flate_ps(pix, options, x_pt, y_pt, width_pt, height_pt),
}
}
fn generate_uncompressed_ps(
pix: &Pix,
options: &PsOptions,
x_pt: f32,
y_pt: f32,
width_pt: f32,
height_pt: f32,
) -> IoResult<String> {
let width = pix.width();
let height = pix.height();
let (image_data, samples_per_pixel, bits_per_sample) = prepare_image_data(pix)?;
let bytes_per_line = if bits_per_sample == 1 {
width.div_ceil(8)
} else {
width * samples_per_pixel
} as usize;
let hex_data = bytes_to_hex(&image_data, bytes_per_line, height as usize);
let mut ps = String::new();
ps.push_str("%!Adobe-PS\n");
if options.write_bounding_box {
ps.push_str(&format!(
"%%BoundingBox: {:.2} {:.2} {:.2} {:.2}\n",
x_pt,
y_pt,
x_pt + width_pt,
y_pt + height_pt
));
} else {
ps.push_str("gsave\n");
}
if bits_per_sample == 1 {
ps.push_str("{1 exch sub} settransfer %invert binary\n");
}
ps.push_str(&format!(
"/bpl {} string def %bpl as a string\n",
bytes_per_line
));
ps.push_str(&format!(
"{:.2} {:.2} translate %set image origin in pts\n",
x_pt, y_pt
));
ps.push_str(&format!(
"{:.2} {:.2} scale %set image size in pts\n",
width_pt, height_pt
));
ps.push_str(&format!(
"{} {} {} %image dimensions in pixels\n",
width, height, bits_per_sample
));
ps.push_str(&format!(
"[{} {} {} {} {} {}] %mapping matrix: [w 0 0 -h 0 h]\n",
width,
0,
0,
-(height as i32),
0,
height
));
if samples_per_pixel == 3 {
if options.write_bounding_box {
ps.push_str("{currentfile bpl readhexstring pop} false 3 colorimage\n");
} else {
ps.push_str("{currentfile bpl readhexstring pop} bind false 3 colorimage\n");
}
} else if options.write_bounding_box {
ps.push_str("{currentfile bpl readhexstring pop} image\n");
} else {
ps.push_str("{currentfile bpl readhexstring pop} bind image\n");
}
ps.push_str(&hex_data);
if options.write_bounding_box {
ps.push_str("\nshowpage\n");
} else {
ps.push_str("\ngrestore\n");
}
Ok(ps)
}
fn generate_flate_ps(
pix: &Pix,
options: &PsOptions,
x_pt: f32,
y_pt: f32,
width_pt: f32,
height_pt: f32,
) -> IoResult<String> {
let width = pix.width();
let height = pix.height();
let (image_data, samples_per_pixel, bits_per_sample) = prepare_image_data(pix)?;
let compressed = compress_to_vec_zlib(&image_data, 6);
let encoded = ascii85::encode(&compressed);
let page_no = options.page_number;
let mut ps = String::new();
ps.push_str("%!PS-Adobe-3.0 EPSF-3.0\n");
ps.push_str("%%Creator: leptonica-rs\n");
if let Some(ref title) = options.title {
ps.push_str(&format!("%%Title: {}\n", title));
} else {
ps.push_str("%%Title: Flate compressed PS\n");
}
ps.push_str("%%DocumentData: Clean7Bit\n");
if options.write_bounding_box {
ps.push_str(&format!(
"%%BoundingBox: {:.2} {:.2} {:.2} {:.2}\n",
x_pt,
y_pt,
x_pt + width_pt,
y_pt + height_pt
));
}
ps.push_str("%%LanguageLevel: 3\n");
ps.push_str("%%EndComments\n");
ps.push_str(&format!("%%Page: {} {}\n", page_no, page_no));
ps.push_str("save\n");
ps.push_str(&format!(
"{:.2} {:.2} translate %set image origin in pts\n",
x_pt, y_pt
));
ps.push_str(&format!(
"{:.2} {:.2} scale %set image size in pts\n",
width_pt, height_pt
));
if samples_per_pixel == 1 {
ps.push_str("/DeviceGray setcolorspace\n");
} else {
ps.push_str("/DeviceRGB setcolorspace\n");
}
ps.push_str("/RawData currentfile /ASCII85Decode filter def\n");
ps.push_str("/Data RawData << >> /FlateDecode filter def\n");
ps.push_str("{ << /ImageType 1\n");
ps.push_str(&format!(" /Width {}\n", width));
ps.push_str(&format!(" /Height {}\n", height));
ps.push_str(&format!(" /BitsPerComponent {}\n", bits_per_sample));
ps.push_str(&format!(
" /ImageMatrix [ {} 0 0 {} 0 {} ]\n",
width,
-(height as i32),
height
));
if samples_per_pixel == 1 {
if bits_per_sample == 1 {
ps.push_str(" /Decode [1 0]\n");
} else {
ps.push_str(" /Decode [0 1]\n");
}
} else {
ps.push_str(" /Decode [0 1 0 1 0 1]\n");
}
ps.push_str(" /DataSource Data\n");
ps.push_str(" >> image\n");
ps.push_str(" Data closefile\n");
ps.push_str(" RawData flushfile\n");
ps.push_str(" showpage\n");
ps.push_str(" restore\n");
ps.push_str("} exec\n");
ps.push_str(&encoded);
ps.push('\n');
Ok(ps)
}
fn generate_dct_ps(
pix: &Pix,
options: &PsOptions,
x_pt: f32,
y_pt: f32,
width_pt: f32,
height_pt: f32,
) -> IoResult<String> {
#[cfg(not(feature = "jpeg"))]
{
let _ = (pix, options, x_pt, y_pt, width_pt, height_pt);
return Err(IoError::UnsupportedFormat(
"Level 2 DCT requires jpeg feature".to_string(),
));
}
#[cfg(feature = "jpeg")]
{
let width = pix.width();
let height = pix.height();
let (image_data, samples_per_pixel, _bits_per_sample) = prepare_image_data(pix)?;
let quality = options.quality.clamp(1, 100);
let color_type = if samples_per_pixel == 1 {
jpeg_encoder::ColorType::Luma
} else {
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 PS error: {}", e)))?;
let encoded = ascii85::encode(&jpeg_buf);
let page_no = options.page_number;
let mut ps = String::new();
ps.push_str("%!PS-Adobe-3.0 EPSF-3.0\n");
ps.push_str("%%Creator: leptonica-rs\n");
if let Some(ref title) = options.title {
ps.push_str(&format!("%%Title: {}\n", title));
}
ps.push_str("%%DocumentData: Clean7Bit\n");
if options.write_bounding_box {
ps.push_str(&format!(
"%%BoundingBox: {:.2} {:.2} {:.2} {:.2}\n",
x_pt,
y_pt,
x_pt + width_pt,
y_pt + height_pt
));
}
ps.push_str("%%LanguageLevel: 2\n");
ps.push_str("%%EndComments\n");
ps.push_str(&format!("%%Page: {} {}\n", page_no, page_no));
ps.push_str("save\n");
ps.push_str(&format!(
"{:.2} {:.2} translate %set image origin in pts\n",
x_pt, y_pt
));
ps.push_str(&format!(
"{:.2} {:.2} scale %set image size in pts\n",
width_pt, height_pt
));
if samples_per_pixel == 1 {
ps.push_str("/DeviceGray setcolorspace\n");
} else {
ps.push_str("/DeviceRGB setcolorspace\n");
}
ps.push_str("/RawData currentfile /ASCII85Decode filter def\n");
ps.push_str("/Data RawData << >> /DCTDecode filter def\n");
ps.push_str("{ << /ImageType 1\n");
ps.push_str(&format!(" /Width {}\n", width));
ps.push_str(&format!(" /Height {}\n", height));
ps.push_str(" /BitsPerComponent 8\n");
ps.push_str(&format!(
" /ImageMatrix [ {} 0 0 {} 0 {} ]\n",
width,
-(height as i32),
height
));
if samples_per_pixel == 1 {
ps.push_str(" /Decode [0 1]\n");
} else {
ps.push_str(" /Decode [0 1 0 1 0 1]\n");
}
ps.push_str(" /DataSource Data\n");
ps.push_str(" >> image\n");
ps.push_str(" Data closefile\n");
ps.push_str(" RawData flushfile\n");
ps.push_str(" showpage\n");
ps.push_str(" restore\n");
ps.push_str("} exec\n");
ps.push_str(&encoded);
ps.push('\n');
Ok(ps)
}
}
fn prepare_image_data(pix: &Pix) -> IoResult<(Vec<u8>, u32, u32)> {
let width = pix.width();
let height = pix.height();
match pix.depth() {
PixelDepth::Bit1 => {
let bytes_per_line = width.div_ceil(8) as usize;
let mut data = vec![0u8; bytes_per_line * height as usize];
for y in 0..height {
for x in 0..width {
let val = pix.get_pixel(x, y).unwrap_or(0);
if val != 0 {
let byte_idx = (y as usize * bytes_per_line) + (x / 8) as usize;
let bit_idx = 7 - (x % 8);
data[byte_idx] |= 1 << bit_idx;
}
}
}
Ok((data, 1, 1))
}
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, 1, 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, 3, 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, 1, 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, 1, 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, 3, 8))
}
}
}
fn bytes_to_hex(data: &[u8], bytes_per_line: usize, num_lines: usize) -> String {
let hex_chars: [char; 16] = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f',
];
let mut result = String::with_capacity(data.len() * 2 + num_lines);
for (i, &byte) in data.iter().enumerate() {
result.push(hex_chars[(byte >> 4) as usize]);
result.push(hex_chars[(byte & 0x0f) as usize]);
if (i + 1) % bytes_per_line == 0 {
result.push('\n');
}
}
result
}
#[allow(dead_code)]
pub fn get_res_letter_page(width: u32, height: u32, fill_fraction: f32) -> u32 {
let fill = if fill_fraction <= 0.0 {
0.95
} else {
fill_fraction as f64
};
let res_w = (width as f64 * 72.0) / (LETTER_WIDTH as f64 * fill);
let res_h = (height as f64 * 72.0) / (LETTER_HEIGHT as f64 * fill);
res_w.max(res_h) as u32
}
pub fn convert_files_to_ps(
dir: impl AsRef<Path>,
substr: Option<&str>,
resolution: u32,
output: impl AsRef<Path>,
) -> IoResult<()> {
let paths = collect_ps_image_files(dir.as_ref(), substr)?;
if paths.is_empty() {
return Err(IoError::InvalidData("no image files found".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 options = PsOptions {
resolution,
..Default::default()
};
let file = std::fs::File::create(output).map_err(IoError::Io)?;
write_ps_multi(&image_refs, file, &options)
}
pub fn convert_files_fitted_to_ps(
dir: impl AsRef<Path>,
substr: Option<&str>,
_xpts: u32,
_ypts: u32,
output: impl AsRef<Path>,
) -> IoResult<()> {
let paths = collect_ps_image_files(dir.as_ref(), substr)?;
if paths.is_empty() {
return Err(IoError::InvalidData("no image files found".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 first = &images[0];
let res = get_res_letter_page(first.width(), first.height(), 0.95);
let options = PsOptions {
resolution: res,
..Default::default()
};
let file = std::fs::File::create(output).map_err(IoError::Io)?;
write_ps_multi(&image_refs, file, &options)
}
pub fn write_image_compressed_to_ps_file(
input: impl AsRef<Path>,
output: impl AsRef<Path>,
resolution: u32,
index: u32,
) -> IoResult<u32> {
let pix = crate::io::read_image(input)?;
let page_no = index + 1;
let options = PsOptions {
resolution,
page_number: page_no,
write_bounding_box: false,
..Default::default()
};
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(index > 0)
.write(true)
.truncate(index == 0)
.open(output)
.map_err(IoError::Io)?;
let ps_string = generate_ps(&pix, &options)?;
file.write_all(ps_string.as_bytes()).map_err(IoError::Io)?;
Ok(page_no)
}
pub fn convert_segmented_pages_to_ps(
dir: impl AsRef<Path>,
substr: Option<&str>,
textscale: f32,
_imagescale: f32,
_threshold: u32,
output: impl AsRef<Path>,
) -> IoResult<()> {
let paths = collect_ps_image_files(dir.as_ref(), substr)?;
if paths.is_empty() {
return Err(IoError::InvalidData("no image files found".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 options = PsOptions {
scale: textscale,
..Default::default()
};
let file = std::fs::File::create(output).map_err(IoError::Io)?;
write_ps_multi(&image_refs, file, &options)
}
pub fn pix_write_segmented_page_to_ps(
pix: &Pix,
_mask: Option<&Pix>,
textscale: f32,
_imagescale: f32,
_threshold: u32,
pageno: u32,
output: impl AsRef<Path>,
) -> IoResult<()> {
let options = PsOptions {
page_number: pageno,
scale: textscale,
write_bounding_box: false,
..Default::default()
};
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(pageno > 1)
.write(true)
.truncate(pageno <= 1)
.open(output)
.map_err(IoError::Io)?;
let ps_string = generate_ps(pix, &options)?;
file.write_all(ps_string.as_bytes()).map_err(IoError::Io)?;
Ok(())
}
pub fn pix_write_mixed_to_ps(
pix_text: Option<&Pix>,
pix_image: Option<&Pix>,
scale: f32,
pageno: u32,
output: impl AsRef<Path>,
) -> IoResult<()> {
let pix = pix_text
.or(pix_image)
.ok_or_else(|| IoError::InvalidData("at least one pix required".to_string()))?;
let options = PsOptions {
page_number: pageno,
scale,
write_bounding_box: false,
..Default::default()
};
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(pageno > 1)
.write(true)
.truncate(pageno <= 1)
.open(output)
.map_err(IoError::Io)?;
let ps_string = generate_ps(pix, &options)?;
file.write_all(ps_string.as_bytes()).map_err(IoError::Io)?;
Ok(())
}
pub fn convert_to_ps_embed(
input: impl AsRef<Path>,
output: impl AsRef<Path>,
level: PsLevel,
) -> IoResult<()> {
let pix = crate::io::read_image(input)?;
let options = PsOptions {
level,
write_bounding_box: true,
..Default::default()
};
let file = std::fs::File::create(output).map_err(IoError::Io)?;
write_ps(&pix, file, &options)
}
pub fn pix_write_compressed_to_ps(
pix: &Pix,
output: impl AsRef<Path>,
resolution: u32,
level: PsLevel,
index: u32,
) -> IoResult<u32> {
let page_no = index + 1;
let options = PsOptions {
level,
resolution,
page_number: page_no,
write_bounding_box: false,
..Default::default()
};
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(index > 0)
.write(true)
.truncate(index == 0)
.open(output)
.map_err(IoError::Io)?;
let ps_string = generate_ps(pix, &options)?;
file.write_all(ps_string.as_bytes()).map_err(IoError::Io)?;
Ok(page_no)
}
pub fn pix_write_string_ps(
pix: &Pix,
_bounding_box: Option<&crate::core::Box>,
resolution: u32,
scale: f32,
) -> IoResult<String> {
let options = PsOptions {
resolution,
scale,
write_bounding_box: true,
level: PsLevel::Level1,
..Default::default()
};
generate_ps(pix, &options)
}
pub fn generate_uncompressed_ps_from_pix(pix: &Pix, resolution: u32) -> IoResult<String> {
let options = PsOptions {
level: PsLevel::Level1,
resolution,
write_bounding_box: true,
..Default::default()
};
generate_ps(pix, &options)
}
pub fn convert_jpeg_to_ps_embed(input: impl AsRef<Path>, output: impl AsRef<Path>) -> IoResult<()> {
convert_to_ps_embed(input, output, PsLevel::Level2)
}
#[allow(clippy::too_many_arguments)]
pub fn convert_jpeg_to_ps(
input: impl AsRef<Path>,
output: impl AsRef<Path>,
_operation: &str,
_x: i32,
_y: i32,
resolution: u32,
scale: f32,
pageno: u32,
endpage: bool,
) -> IoResult<()> {
let pix = crate::io::read_image(input)?;
let options = PsOptions {
level: PsLevel::Level2,
resolution,
scale,
page_number: pageno,
write_bounding_box: endpage,
..Default::default()
};
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(pageno > 1)
.write(true)
.truncate(pageno <= 1)
.open(output)
.map_err(IoError::Io)?;
let ps_string = generate_ps(&pix, &options)?;
file.write_all(ps_string.as_bytes()).map_err(IoError::Io)?;
Ok(())
}
#[cfg(feature = "tiff-format")]
pub fn convert_g4_to_ps_embed(input: impl AsRef<Path>, output: impl AsRef<Path>) -> IoResult<()> {
convert_to_ps_embed(input, output, PsLevel::Level3)
}
#[allow(clippy::too_many_arguments)]
#[cfg(feature = "tiff-format")]
pub fn convert_g4_to_ps(
input: impl AsRef<Path>,
output: impl AsRef<Path>,
_operation: &str,
_x: i32,
_y: i32,
resolution: u32,
scale: f32,
pageno: u32,
_maskflag: bool,
endpage: bool,
) -> IoResult<()> {
let pix = crate::io::read_image(input)?;
let options = PsOptions {
level: PsLevel::Level3,
resolution,
scale,
page_number: pageno,
write_bounding_box: endpage,
..Default::default()
};
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(pageno > 1)
.write(true)
.truncate(pageno <= 1)
.open(output)
.map_err(IoError::Io)?;
let ps_string = generate_ps(&pix, &options)?;
file.write_all(ps_string.as_bytes()).map_err(IoError::Io)?;
Ok(())
}
#[cfg(feature = "tiff-format")]
pub fn convert_tiff_multipage_to_ps(
input: impl AsRef<Path>,
output: impl AsRef<Path>,
fill_fraction: f32,
) -> IoResult<()> {
let data = std::fs::read(input.as_ref()).map_err(IoError::Io)?;
let cursor = std::io::Cursor::new(&data);
let pages = crate::io::tiff::read_tiff_multipage(cursor)?;
if pages.is_empty() {
return Err(IoError::InvalidData("no pages in TIFF".to_string()));
}
let first = &pages[0];
let fill = if fill_fraction <= 0.0 {
0.95
} else {
fill_fraction
};
let res = get_res_letter_page(first.width(), first.height(), fill);
let page_refs: Vec<&Pix> = pages.iter().collect();
let options = PsOptions {
resolution: res,
..Default::default()
};
let file = std::fs::File::create(output).map_err(IoError::Io)?;
write_ps_multi(&page_refs, file, &options)
}
pub fn convert_flate_to_ps_embed(
input: impl AsRef<Path>,
output: impl AsRef<Path>,
) -> IoResult<()> {
convert_to_ps_embed(input, output, PsLevel::Level3)
}
#[allow(clippy::too_many_arguments)]
pub fn convert_flate_to_ps(
input: impl AsRef<Path>,
output: impl AsRef<Path>,
_operation: &str,
_x: i32,
_y: i32,
resolution: u32,
scale: f32,
pageno: u32,
endpage: bool,
) -> IoResult<()> {
let pix = crate::io::read_image(input)?;
let options = PsOptions {
level: PsLevel::Level3,
resolution,
scale,
page_number: pageno,
write_bounding_box: endpage,
..Default::default()
};
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(pageno > 1)
.write(true)
.truncate(pageno <= 1)
.open(output)
.map_err(IoError::Io)?;
let ps_string = generate_ps(&pix, &options)?;
file.write_all(ps_string.as_bytes()).map_err(IoError::Io)?;
Ok(())
}
fn collect_ps_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)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::PixelDepth;
#[test]
fn test_write_ps_grayscale_level1() {
let pix = Pix::new(50, 50, PixelDepth::Bit8).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
for y in 0..50 {
for x in 0..50 {
pix_mut.set_pixel(x, y, (x + y) % 256).unwrap();
}
}
let pix: Pix = pix_mut.into();
let options = PsOptions::default().level(PsLevel::Level1);
let ps_data = write_ps_mem(&pix, &options).unwrap();
let ps_str = String::from_utf8_lossy(&ps_data);
assert!(ps_str.starts_with("%!Adobe-PS"));
assert!(ps_str.contains("BoundingBox"));
assert!(ps_str.contains("image"));
}
#[test]
fn test_write_ps_grayscale_level3() {
let pix = Pix::new(50, 50, PixelDepth::Bit8).unwrap();
let options = PsOptions::default().level(PsLevel::Level3);
let ps_data = write_ps_mem(&pix, &options).unwrap();
let ps_str = String::from_utf8_lossy(&ps_data);
assert!(ps_str.starts_with("%!PS-Adobe-3.0"));
assert!(ps_str.contains("LanguageLevel: 3"));
assert!(ps_str.contains("FlateDecode"));
assert!(ps_str.contains("ASCII85Decode"));
assert!(ps_str.contains("~>")); }
#[test]
fn test_write_ps_rgb() {
let pix = Pix::new(30, 30, PixelDepth::Bit32).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
for y in 0..30 {
for x in 0..30 {
let color = pixel::compose_rgb((x * 8) as u8, (y * 8) as u8, 128);
pix_mut.set_pixel(x, y, color).unwrap();
}
}
let pix: Pix = pix_mut.into();
let options = PsOptions::with_title("RGB Test");
let ps_data = write_ps_mem(&pix, &options).unwrap();
let ps_str = String::from_utf8_lossy(&ps_data);
assert!(ps_str.contains("DeviceRGB"));
assert!(ps_str.contains("Title: RGB Test"));
}
#[test]
fn test_write_ps_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 = PsOptions::default();
let ps_data = write_ps_mem(&pix, &options).unwrap();
let ps_str = String::from_utf8_lossy(&ps_data);
assert!(ps_str.contains("DeviceGray"));
}
#[test]
fn test_write_eps() {
let pix = Pix::new(100, 100, PixelDepth::Bit8).unwrap();
let options = PsOptions::eps();
let eps_data = write_eps_mem(&pix, &options).unwrap();
let eps_str = String::from_utf8_lossy(&eps_data);
assert!(eps_str.contains("EPSF-3.0"));
assert!(eps_str.contains("BoundingBox"));
}
#[test]
fn test_ps_options() {
let opts = PsOptions::default();
assert_eq!(opts.level, PsLevel::Level3);
assert_eq!(opts.resolution, 0);
assert!(opts.write_bounding_box);
assert_eq!(opts.scale, 1.0);
let opts = PsOptions::with_title("Test")
.resolution(150)
.level(PsLevel::Level1)
.bounding_box(false);
assert_eq!(opts.title, Some("Test".to_string()));
assert_eq!(opts.resolution, 150);
assert_eq!(opts.level, PsLevel::Level1);
assert!(!opts.write_bounding_box);
}
#[test]
fn test_get_res_letter_page() {
let res = get_res_letter_page(612, 792, 0.95);
assert!(res > 70 && res < 80);
let res = get_res_letter_page(2550, 3300, 0.95);
assert!(res > 310 && res < 320);
}
#[test]
fn test_bytes_to_hex() {
let data = vec![0x00, 0xFF, 0xAB, 0xCD];
let hex = bytes_to_hex(&data, 2, 2);
assert!(hex.contains("00ff"));
assert!(hex.contains("abcd"));
}
#[test]
fn test_write_ps_multi_pages() {
let pix1 = Pix::new(100, 100, PixelDepth::Bit8).unwrap();
let pix2 = Pix::new(200, 150, PixelDepth::Bit32).unwrap();
let pix3 = Pix::new(50, 50, PixelDepth::Bit1).unwrap();
let images: Vec<&Pix> = vec![&pix1, &pix2, &pix3];
let options = PsOptions::with_title("Multi-page Test");
let mut buffer = Vec::new();
write_ps_multi(&images, &mut buffer, &options).unwrap();
let ps_str = String::from_utf8_lossy(&buffer);
assert!(ps_str.starts_with("%!PS-Adobe"));
assert!(ps_str.contains("%%Page: 1 1"));
assert!(ps_str.contains("%%Page: 2 2"));
assert!(ps_str.contains("%%Page: 3 3"));
assert!(ps_str.contains("%%Pages: 3"));
}
#[test]
fn test_write_ps_level2_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 c = pixel::compose_rgb(x as u8 * 5, y as u8 * 5, 128);
pix_mut.set_pixel(x, y, c).unwrap();
}
}
let pix: Pix = pix_mut.into();
let options = PsOptions::default().level(PsLevel::Level2);
let ps_data = write_ps_mem(&pix, &options).unwrap();
let ps_str = String::from_utf8_lossy(&ps_data);
assert!(ps_str.contains("DCTDecode"));
assert!(ps_str.contains("LanguageLevel: 2"));
}
#[test]
fn test_write_ps_level2_grayscale() {
let pix = Pix::new(60, 60, PixelDepth::Bit8).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
for y in 0..60 {
for x in 0..60 {
pix_mut.set_pixel(x, y, ((x + y) * 3) % 256).unwrap();
}
}
let pix: Pix = pix_mut.into();
let options = PsOptions::default().level(PsLevel::Level2);
let ps_data = write_ps_mem(&pix, &options).unwrap();
let ps_str = String::from_utf8_lossy(&ps_data);
assert!(ps_str.contains("DCTDecode"));
assert!(ps_str.contains("DeviceGray"));
}
#[test]
fn test_write_ps_level2_1bpp_fallback() {
let pix = Pix::new(80, 80, PixelDepth::Bit1).unwrap();
let options = PsOptions::default().level(PsLevel::Level2);
let ps_data = write_ps_mem(&pix, &options).unwrap();
let ps_str = String::from_utf8_lossy(&ps_data);
assert!(ps_str.contains("FlateDecode"));
assert!(!ps_str.contains("DCTDecode"));
}
}