use std::path::Path;
use pdfboss_core::{Document, Error as CoreError, Permissions};
use pdfboss_write::{
decrypt_document, encrypt_document, merge_documents, rewrite_document, rotate_pages,
rotate_rewrite, split_document, watermark, watermark_under, watermark_under_with,
watermark_with, Error as WriteError, Update, WriteOptions,
};
use crate::pages::{parse_ranges, pattern_path, split_input_spec};
use crate::Failure;
pub fn cmd_merge(inputs: &[String], out: &Path, password: &str) -> Result<(), String> {
let mut sources = Vec::with_capacity(inputs.len());
for spec in inputs {
let (path, range) = split_input_spec(spec);
let doc = Document::open_with_password(&path, password)
.map_err(|e| format!("{}: {e}", path.display()))?;
reject_encrypted(&doc, &path)?;
let indices = match range {
Some(text) => Some(
parse_ranges(&text, doc.page_count())
.map_err(|e| format!("{}: {e}", path.display()))?,
),
None => None,
};
sources.push((doc, indices));
}
let selection: Vec<(&Document, Option<&[usize]>)> = sources
.iter()
.map(|(doc, indices)| (doc, indices.as_deref()))
.collect();
let count: usize = sources
.iter()
.map(|(doc, indices)| indices.as_ref().map_or(doc.page_count(), Vec::len))
.sum();
let bytes = merge_documents(&selection, WriteOptions::default()).map_err(|e| e.to_string())?;
std::fs::write(out, bytes).map_err(|e| format!("{}: {e}", out.display()))?;
let plural = if count == 1 { "" } else { "s" };
println!("wrote {} ({count} page{plural})", out.display());
Ok(())
}
pub fn cmd_split(file: &Path, out: &str, every: usize, password: &str) -> Result<(), String> {
pattern_path(out, 1)?;
let doc = Document::open_with_password(file, password)
.map_err(|e| format!("{}: {e}", file.display()))?;
reject_encrypted(&doc, file)?;
let total = doc.page_count();
let parts = split_document(&doc, every, WriteOptions::default()).map_err(|e| e.to_string())?;
for (i, bytes) in parts.iter().enumerate() {
let path = pattern_path(out, i + 1)?;
std::fs::write(&path, bytes).map_err(|e| format!("{}: {e}", path.display()))?;
let start = i * every;
let count = (start + every).min(total) - start;
let plural = if count == 1 { "" } else { "s" };
println!("wrote {} ({count} page{plural})", path.display());
}
Ok(())
}
pub fn cmd_rotate(
file: &Path,
out: &Path,
pages: Option<&str>,
by: &str,
rewrite: bool,
password: &str,
) -> Result<(), String> {
let by: i32 = by
.parse()
.map_err(|_| format!("invalid --by value: {by}"))?;
let doc = Document::open_with_password(file, password)
.map_err(|e| format!("{}: {e}", file.display()))?;
reject_encrypted(&doc, file)?;
let indices = match pages {
Some(text) => {
parse_ranges(text, doc.page_count()).map_err(|e| format!("{}: {e}", file.display()))?
}
None => (0..doc.page_count()).collect(),
};
let count = indices.len();
if rewrite {
let bytes = rotate_rewrite(&doc, &indices, by, WriteOptions::default())
.map_err(|e| e.to_string())?;
std::fs::write(out, bytes).map_err(|e| format!("{}: {e}", out.display()))?;
} else {
let mut update = Update::new(&doc).map_err(|e| e.to_string())?;
rotate_pages(&mut update, &indices, by).map_err(|e| e.to_string())?;
update
.save(out)
.map_err(|e| format!("{}: {e}", out.display()))?;
}
let plural = if count == 1 { "" } else { "s" };
println!("wrote {} ({count} page{plural} rotated)", out.display());
Ok(())
}
pub fn cmd_rewrite(file: &Path, out: &Path, password: &str) -> Result<(), String> {
let doc = Document::open_with_password(file, password)
.map_err(|e| format!("{}: {e}", file.display()))?;
reject_encrypted(&doc, file)?;
let bytes = rewrite_document(&doc, WriteOptions::default()).map_err(|e| e.to_string())?;
std::fs::write(out, bytes).map_err(|e| format!("{}: {e}", out.display()))?;
println!("wrote {}", out.display());
Ok(())
}
pub fn cmd_encrypt(
file: &Path,
out: &Path,
user_password: &str,
owner_password: &str,
allow: Option<Vec<String>>,
password: &str,
) -> Result<(), Failure> {
if user_password.is_empty() && owner_password.is_empty() {
return Err(Failure::new(
"at least one of --user-password or --owner-password must be set",
));
}
let permissions = parse_allow(allow)?;
let doc = Document::open_with_password(file, password).map_err(|e| match e {
CoreError::Encrypted => {
Failure::new(format!("{}: wrong or missing password", file.display()))
}
other => Failure::new(format!("{}: {other}", file.display())),
})?;
let bytes = encrypt_document(
&doc,
user_password,
owner_password,
permissions,
WriteOptions::default(),
)
.map_err(|e| Failure::new(e.to_string()))?;
std::fs::write(out, bytes).map_err(|e| Failure::new(format!("{}: {e}", out.display())))?;
println!("wrote {}", out.display());
Ok(())
}
pub fn cmd_decrypt(file: &Path, out: &Path, password: &str) -> Result<(), String> {
let doc = Document::open_with_password(file, password).map_err(|e| match e {
CoreError::Encrypted => format!("{}: wrong or missing password", file.display()),
other => format!("{}: {other}", file.display()),
})?;
let bytes = decrypt_document(&doc, WriteOptions::default()).map_err(|e| e.to_string())?;
std::fs::write(out, bytes).map_err(|e| format!("{}: {e}", out.display()))?;
println!("wrote {}", out.display());
Ok(())
}
fn parse_allow(values: Option<Vec<String>>) -> Result<Permissions, Failure> {
let Some(values) = values else {
return Ok(Permissions::all());
};
Permissions::from_names(values.iter().map(String::as_str)).map_err(|bad| {
Failure::program(format!(
"invalid value '{bad}' for --allow: expected one of {}",
pdfboss_core::PERMISSION_NAMES.join(", ")
))
})
}
pub fn cmd_overlay(
file: &Path,
overlay: &Path,
out: &Path,
under: bool,
rewrite: bool,
password: &str,
) -> Result<(), String> {
let doc = Document::open_with_password(file, password)
.map_err(|e| format!("{}: {e}", file.display()))?;
reject_encrypted(&doc, file)?;
let mark = Document::open_with_password(overlay, password)
.map_err(|e| format!("{}: {e}", overlay.display()))?;
reject_encrypted(&mark, overlay)?;
let bytes = match (under, rewrite) {
(false, false) => watermark(&doc, &mark),
(true, false) => watermark_under(&doc, &mark),
(false, true) => watermark_with(&doc, &mark, WriteOptions::default()),
(true, true) => watermark_under_with(&doc, &mark, WriteOptions::default()),
}
.map_err(|e| e.to_string())?;
std::fs::write(out, bytes).map_err(|e| format!("{}: {e}", out.display()))?;
println!("wrote {}", out.display());
Ok(())
}
pub fn reject_encrypted(doc: &Document, path: &Path) -> Result<(), String> {
if doc.is_encrypted() {
return Err(format!("{}: {}", path.display(), WriteError::EncryptedBase));
}
Ok(())
}