extern crate lopdf;
#[macro_use]
extern crate bitflags;
#[macro_use]
extern crate derive_error;
use lopdf::{Document, Object, ObjectId, StringFormat};
use std::collections::VecDeque;
use std::io;
use std::io::Write;
use std::path::Path;
use std::str;
bitflags! {
struct ButtonFlags: u32 {
const NO_TOGGLE_TO_OFF = 0x8000;
const RADIO = 0x10000;
const PUSHBUTTON = 0x20000;
const RADIO_IN_UNISON = 0x4000000;
}
}
bitflags! {
struct ChoiceFlags: u32 {
const COBMO = 0x20000;
const EDIT = 0x40000;
const SORT = 0x80000;
const MULTISELECT = 0x200000;
const DO_NOT_SPELLCHECK = 0x800000;
const COMMIT_ON_CHANGE = 0x8000000;
}
}
pub struct Form {
doc: Document,
form_ids: Vec<ObjectId>,
}
#[derive(Debug)]
pub enum FieldType {
Button,
Radio,
CheckBox,
ListBox,
ComboBox,
Text,
}
#[derive(Debug)]
pub enum FieldState {
Button,
Radio {
selected: String,
options: Vec<String>,
},
CheckBox { is_checked: bool },
ListBox {
selected: Vec<String>,
options: Vec<String>,
multiselect: bool,
},
ComboBox {
selected: Vec<String>,
options: Vec<String>,
editable: bool,
},
Text { text: String },
}
#[derive(Debug, Error)]
pub enum LoadError {
IoError(io::Error),
DictionaryKeyNotFound,
#[error(non_std, no_from)]
NoSuchReference(ObjectId),
NotAReference,
UnexpectedType,
}
#[derive(Debug, Error)]
pub enum ValueError {
TypeMismatch,
InvalidSelection,
TooManySelected,
}
trait PdfObjectDeref {
fn deref<'a>(&self, doc: &'a Document) -> Result<&'a Object, LoadError>;
}
impl PdfObjectDeref for Object {
fn deref<'a>(&self, doc: &'a Document) -> Result<&'a Object, LoadError> {
match self {
&Object::Reference(oid) => doc.objects.get(&oid).ok_or(LoadError::NoSuchReference(oid)),
_ => Err(LoadError::NotAReference),
}
}
}
impl Form {
pub fn load_from<R: io::Read>(reader: R) -> Result<Self, LoadError> {
let doc = Document::load_from(reader)?;
Self::load_doc(doc)
}
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, LoadError> {
let doc = Document::load(path)?;
Self::load_doc(doc)
}
fn load_doc(doc: Document) -> Result<Self, LoadError> {
let mut form_ids = Vec::new();
let mut queue = VecDeque::new();
{
let catalog = doc
.trailer
.get("Root")
.ok_or(LoadError::DictionaryKeyNotFound)?
.deref(&doc)?
.as_dict()
.ok_or(LoadError::UnexpectedType)?;
let acroform = catalog
.get("AcroForm")
.ok_or(LoadError::DictionaryKeyNotFound)?
.deref(&doc)?
.as_dict()
.ok_or(LoadError::UnexpectedType)?;
let fields_list = acroform
.get("Fields")
.ok_or(LoadError::DictionaryKeyNotFound)?
.as_array()
.ok_or(LoadError::UnexpectedType)?;
queue.append(&mut VecDeque::from(fields_list.clone()));
while let Some(objref) = queue.pop_front() {
let obj = objref.deref(&doc)?;
if let &Object::Dictionary(ref dict) = obj {
if let Some(_) = dict.get("FT") {
form_ids.push(objref.as_reference().unwrap());
}
if let Some(&Object::Array(ref kids)) = dict.get("Kids") {
queue.append(&mut VecDeque::from(kids.clone()));
}
}
}
}
Ok(Form { doc, form_ids })
}
pub fn len(&self) -> usize {
self.form_ids.len()
}
pub fn get_type(&self, n: usize) -> FieldType {
let field = self
.doc
.objects
.get(&self.form_ids[n])
.unwrap()
.as_dict()
.unwrap();
let obj_zero = Object::Integer(0);
let type_str = field.get("FT").unwrap().as_name_str().unwrap();
if type_str == "Btn" {
let flags = ButtonFlags::from_bits_truncate(
field.get("Ff").unwrap_or(&obj_zero).as_i64().unwrap() as u32,
);
if flags.intersects(ButtonFlags::RADIO | ButtonFlags::NO_TOGGLE_TO_OFF) {
FieldType::Radio
} else if flags.intersects(ButtonFlags::PUSHBUTTON) {
FieldType::Button
} else {
FieldType::CheckBox
}
} else if type_str == "Ch" {
let flags = ChoiceFlags::from_bits_truncate(
field.get("Ff").unwrap_or(&obj_zero).as_i64().unwrap() as u32,
);
if flags.intersects(ChoiceFlags::COBMO) {
FieldType::ComboBox
} else {
FieldType::ListBox
}
} else {
FieldType::Text
}
}
pub fn get_name(&self, n: usize) -> Option<String> {
let field = self
.doc
.objects
.get(&self.form_ids[n])
.unwrap()
.as_dict()
.unwrap();
match field.get("T") {
Some(Object::String(data, _)) => String::from_utf8(data.clone()).ok(),
_ => None,
}
}
pub fn get_all_types(&self) -> Vec<FieldType> {
let mut res = Vec::with_capacity(self.len());
for i in 0..self.len() {
res.push(self.get_type(i))
}
res
}
pub fn get_all_names(&self) -> Vec<Option<String>> {
let mut res = Vec::with_capacity(self.len());
for i in 0..self.len() {
res.push(self.get_name(i))
}
res
}
pub fn get_state(&self, n: usize) -> FieldState {
let field = self
.doc
.objects
.get(&self.form_ids[n])
.unwrap()
.as_dict()
.unwrap();
match self.get_type(n) {
FieldType::Button => FieldState::Button,
FieldType::Radio => FieldState::Radio {
selected: match field.get("V") {
Some(name) => name.as_name_str().unwrap().to_owned(),
None => match field.get("AS") {
Some(name) => name.as_name_str().unwrap().to_owned(),
None => "".to_owned(),
},
},
options: self.get_possibilities(self.form_ids[n]),
},
FieldType::CheckBox => FieldState::CheckBox {
is_checked: match field.get("V") {
Some(name) => {
if name.as_name_str().unwrap() == "Yes" {
true
} else {
false
}
}
None => match field.get("AS") {
Some(name) => {
if name.as_name_str().unwrap() == "Yes" {
true
} else {
false
}
}
None => false,
},
},
},
FieldType::ListBox => FieldState::ListBox {
selected: match field.get("V") {
Some(selection) => match selection {
&Object::String(ref s, StringFormat::Literal) => {
vec![str::from_utf8(&s).unwrap().to_owned()]
}
&Object::Array(ref chosen) => {
let mut res = Vec::new();
for obj in chosen {
if let &Object::String(ref s, StringFormat::Literal) = obj {
res.push(str::from_utf8(&s).unwrap().to_owned());
}
}
res
}
_ => Vec::new(),
},
None => Vec::new(),
},
options: match field.get("Opt") {
Some(&Object::Array(ref options)) => options
.iter()
.map(|x| match x {
&Object::String(ref s, StringFormat::Literal) => {
str::from_utf8(&s).unwrap().to_owned()
}
&Object::Array(ref arr) => {
if let &Object::String(ref s, StringFormat::Literal) = &arr[1] {
str::from_utf8(&s).unwrap().to_owned()
} else {
String::new()
}
}
_ => String::new(),
})
.filter(|x| x.len() > 0)
.collect(),
_ => Vec::new(),
},
multiselect: {
let flags = ChoiceFlags::from_bits_truncate(
field.get("Ff").unwrap_or(&Object::Integer(0)).as_i64().unwrap() as u32,
);
flags.intersects(ChoiceFlags::MULTISELECT)
},
},
FieldType::ComboBox => FieldState::ComboBox {
selected: match field.get("V") {
Some(selection) => match selection {
&Object::String(ref s, StringFormat::Literal) => {
vec![str::from_utf8(&s).unwrap().to_owned()]
}
&Object::Array(ref chosen) => {
let mut res = Vec::new();
for obj in chosen {
if let &Object::String(ref s, StringFormat::Literal) = obj {
res.push(str::from_utf8(&s).unwrap().to_owned());
}
}
res
}
_ => Vec::new(),
},
None => Vec::new(),
},
options: match field.get("Opt") {
Some(&Object::Array(ref options)) => options
.iter()
.map(|x| match x {
&Object::String(ref s, StringFormat::Literal) => {
str::from_utf8(&s).unwrap().to_owned()
}
&Object::Array(ref arr) => {
if let &Object::String(ref s, StringFormat::Literal) = &arr[1] {
str::from_utf8(&s).unwrap().to_owned()
} else {
String::new()
}
}
_ => String::new(),
})
.filter(|x| x.len() > 0)
.collect(),
_ => Vec::new(),
},
editable: {
let flags = ChoiceFlags::from_bits_truncate(
field.get("Ff").unwrap_or(&Object::Integer(0)).as_i64().unwrap() as u32,
);
flags.intersects(ChoiceFlags::EDIT)
},
},
FieldType::Text => FieldState::Text {
text: match field.get("V") {
Some(&Object::String(ref s, StringFormat::Literal)) => {
str::from_utf8(&s.clone()).unwrap().to_owned()
}
_ => "".to_owned(),
},
},
}
}
pub fn set_text(&mut self, n: usize, s: String) -> Result<(), ValueError> {
match self.get_type(n) {
FieldType::Text => {
let field = self
.doc
.objects
.get_mut(&self.form_ids[n])
.unwrap()
.as_dict_mut()
.unwrap();
field.set("V", Object::String(s.into_bytes(), StringFormat::Literal));
field.remove("AP");
Ok(())
}
_ => Err(ValueError::TypeMismatch),
}
}
fn get_possibilities(&self, oid: ObjectId) -> Vec<String> {
let mut res = Vec::new();
let kids_obj = self
.doc
.objects
.get(&oid)
.unwrap()
.as_dict()
.unwrap()
.get("Kids");
if let Some(&Object::Array(ref kids)) = kids_obj {
for (i, kid) in kids.iter().enumerate() {
let mut found = false;
if let Some(&Object::Dictionary(ref appearance_states)) =
kid.deref(&self.doc).unwrap().as_dict().unwrap().get("AP")
{
if let Some(&Object::Dictionary(ref normal_appearance)) =
appearance_states.get("N")
{
for (key, _) in normal_appearance {
if (key != "Off") {
res.push(key.to_owned());
found = true;
break;
}
}
}
}
if !found {
res.push(i.to_string());
}
}
}
res
}
pub fn set_check_box(&mut self, n: usize, is_checked: bool) -> Result<(), ValueError> {
match self.get_type(n) {
FieldType::CheckBox => {
let state = Object::Name(
{
if is_checked {
"Yes"
} else {
"Off"
}
}
.to_owned()
.into_bytes(),
);
let field = self
.doc
.objects
.get_mut(&self.form_ids[n])
.unwrap()
.as_dict_mut()
.unwrap();
field.set("V", state.clone());
field.set("AS", state);
Ok(())
}
_ => Err(ValueError::TypeMismatch),
}
}
pub fn set_radio(&mut self, n: usize, choice: String) -> Result<(), ValueError> {
match self.get_state(n) {
FieldState::Radio {
selected: _,
options,
} => {
if options.contains(&choice) {
let field = self
.doc
.objects
.get_mut(&self.form_ids[n])
.unwrap()
.as_dict_mut()
.unwrap();
field.set("V", Object::Name(choice.into_bytes()));
Ok(())
} else {
Err(ValueError::InvalidSelection)
}
}
_ => Err(ValueError::TypeMismatch),
}
}
pub fn set_list_box(&mut self, n: usize, choices: Vec<String>) -> Result<(), ValueError> {
match self.get_state(n) {
FieldState::ListBox {
selected: _,
options,
multiselect,
} => {
if choices.iter().fold(true, |a, h| options.contains(h) && a) {
if !multiselect && choices.len() > 1 {
Err(ValueError::TooManySelected)
} else {
let field = self
.doc
.objects
.get_mut(&self.form_ids[n])
.unwrap()
.as_dict_mut()
.unwrap();
match choices.len() {
0 => field.set("V", Object::Null),
1 => field.set(
"V",
Object::String(
choices[0].clone().into_bytes(),
StringFormat::Literal,
),
),
_ => field.set(
"V",
Object::Array(
choices
.iter()
.map(|x| {
Object::String(
x.clone().into_bytes(),
StringFormat::Literal,
)
})
.collect(),
),
),
};
Ok(())
}
} else {
Err(ValueError::InvalidSelection)
}
}
_ => Err(ValueError::TypeMismatch),
}
}
pub fn set_combo_box(&mut self, n: usize, choice: String) -> Result<(), ValueError> {
match self.get_state(n) {
FieldState::ComboBox {
selected: _,
options,
editable,
} => {
if options.contains(&choice) || editable {
let field = self
.doc
.objects
.get_mut(&self.form_ids[n])
.unwrap()
.as_dict_mut()
.unwrap();
field.set(
"V",
Object::String(choice.clone().into_bytes(), StringFormat::Literal),
);
Ok(())
} else {
Err(ValueError::InvalidSelection)
}
}
_ => Err(ValueError::TypeMismatch),
}
}
pub fn save<P: AsRef<Path>>(&mut self, path: P) -> Result<(), io::Error> {
self.doc.save(path).map(|_| ())
}
pub fn save_to<W: Write>(&mut self, target: &mut W) -> Result<(), io::Error> {
self.doc.save_to(target)
}
}