use crate::error::Result;
use crate::extractors::forms::{FieldValue, FormField};
use std::io::Write;
use std::path::Path;
#[derive(Debug, Clone)]
pub struct FdfField {
pub name: String,
pub value: FdfValue,
pub kids: Vec<FdfField>,
}
#[derive(Debug, Clone)]
pub enum FdfValue {
Text(String),
Boolean(bool),
Name(String),
Array(Vec<String>),
None,
}
impl From<&FieldValue> for FdfValue {
fn from(value: &FieldValue) -> Self {
match value {
FieldValue::Text(s) => FdfValue::Text(s.clone()),
FieldValue::Boolean(b) => FdfValue::Boolean(*b),
FieldValue::Name(s) => FdfValue::Name(s.clone()),
FieldValue::Array(arr) => FdfValue::Array(arr.clone()),
FieldValue::None => FdfValue::None,
}
}
}
impl FdfField {
pub fn new(name: impl Into<String>, value: FdfValue) -> Self {
Self {
name: name.into(),
value,
kids: Vec::new(),
}
}
pub fn with_kid(mut self, kid: FdfField) -> Self {
self.kids.push(kid);
self
}
fn to_fdf_dict(&self) -> String {
let mut dict = String::new();
dict.push_str("<< ");
dict.push_str("/T ");
dict.push_str(&encode_pdf_string(&self.name));
if !matches!(self.value, FdfValue::None) {
dict.push_str(" /V ");
dict.push_str(&self.value.to_fdf_value());
}
if !self.kids.is_empty() {
dict.push_str(" /Kids [ ");
for kid in &self.kids {
dict.push_str(&kid.to_fdf_dict());
dict.push(' ');
}
dict.push(']');
}
dict.push_str(" >>");
dict
}
}
impl FdfValue {
fn to_fdf_value(&self) -> String {
match self {
FdfValue::Text(s) => encode_pdf_string(s),
FdfValue::Boolean(b) => {
if *b {
"/Yes".to_string()
} else {
"/Off".to_string()
}
},
FdfValue::Name(s) => format!("/{}", s),
FdfValue::Array(arr) => {
let items: Vec<String> = arr.iter().map(|s| encode_pdf_string(s)).collect();
format!("[ {} ]", items.join(" "))
},
FdfValue::None => "null".to_string(),
}
}
}
fn encode_pdf_string(s: &str) -> String {
let mut encoded = String::from("(");
for c in s.chars() {
match c {
'(' => encoded.push_str("\\("),
')' => encoded.push_str("\\)"),
'\\' => encoded.push_str("\\\\"),
'\r' => encoded.push_str("\\r"),
'\n' => encoded.push_str("\\n"),
'\t' => encoded.push_str("\\t"),
_ => encoded.push(c),
}
}
encoded.push(')');
encoded
}
#[derive(Debug, Default)]
pub struct FdfWriter {
fields: Vec<FdfField>,
file_spec: Option<String>,
}
impl FdfWriter {
pub fn new() -> Self {
Self::default()
}
pub fn from_fields(fields: Vec<FormField>) -> Self {
let fdf_fields: Vec<FdfField> = fields
.into_iter()
.map(|f| FdfField::new(f.full_name, FdfValue::from(&f.value)))
.collect();
Self {
fields: fdf_fields,
file_spec: None,
}
}
pub fn with_file_spec(mut self, path: impl Into<String>) -> Self {
self.file_spec = Some(path.into());
self
}
pub fn add_field(&mut self, field: FdfField) {
self.fields.push(field);
}
pub fn write_to_file(&self, path: impl AsRef<Path>) -> Result<()> {
let bytes = self.to_bytes()?;
std::fs::write(path.as_ref(), bytes)?;
Ok(())
}
pub fn to_bytes(&self) -> Result<Vec<u8>> {
let mut output = Vec::new();
writeln!(output, "%FDF-1.2")?;
output.write_all(b"%")?;
output.write_all(&[0xe2, 0xe3, 0xcf, 0xd3])?;
writeln!(output)?;
writeln!(output, "1 0 obj")?;
writeln!(output, "<<")?;
writeln!(output, "/FDF <<")?;
if let Some(ref file_spec) = self.file_spec {
writeln!(output, "/F {}", encode_pdf_string(file_spec))?;
}
writeln!(output, "/Fields [")?;
for field in &self.fields {
writeln!(output, "{}", field.to_fdf_dict())?;
}
writeln!(output, "]")?;
writeln!(output, ">>")?;
writeln!(output, ">>")?;
writeln!(output, "endobj")?;
writeln!(output, "trailer")?;
writeln!(output, "<< /Root 1 0 R >>")?;
writeln!(output, "%%EOF")?;
Ok(output)
}
pub fn to_string(&self) -> Result<String> {
let bytes = self.to_bytes()?;
Ok(String::from_utf8_lossy(&bytes).to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encode_pdf_string() {
assert_eq!(encode_pdf_string("Hello"), "(Hello)");
assert_eq!(encode_pdf_string("Hello (World)"), "(Hello \\(World\\))");
assert_eq!(encode_pdf_string("Line1\nLine2"), "(Line1\\nLine2)");
}
#[test]
fn test_fdf_field_to_dict() {
let field = FdfField::new("name", FdfValue::Text("John".into()));
let dict = field.to_fdf_dict();
assert!(dict.contains("/T (name)"));
assert!(dict.contains("/V (John)"));
}
#[test]
fn test_fdf_writer_to_bytes() {
let mut writer = FdfWriter::new();
writer.add_field(FdfField::new("test", FdfValue::Text("value".into())));
let bytes = writer.to_bytes().unwrap();
let content = String::from_utf8_lossy(&bytes);
assert!(content.contains("%FDF-1.2"));
assert!(content.contains("/Fields"));
assert!(content.contains("/T (test)"));
assert!(content.contains("/V (value)"));
assert!(content.contains("%%EOF"));
}
#[test]
fn test_fdf_boolean_value() {
let field_yes = FdfField::new("check", FdfValue::Boolean(true));
let dict_yes = field_yes.to_fdf_dict();
assert!(dict_yes.contains("/V /Yes"));
let field_no = FdfField::new("check", FdfValue::Boolean(false));
let dict_no = field_no.to_fdf_dict();
assert!(dict_no.contains("/V /Off"));
}
}