From ec35be1f2ded5e90bf4dd8bdb8aaa8d76bf84942 Mon Sep 17 00:00:00 2001 From: Emulator000 Date: Wed, 23 Sep 2020 23:01:05 +0200 Subject: Updated all libraries and a lot of fixes --- src/lib.rs | 538 ++++++++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 371 insertions(+), 167 deletions(-) (limited to 'src') diff --git a/src/lib.rs b/src/lib.rs index 3f1e1c4..670f3ac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,16 +1,17 @@ -//! This crate is for filling out PDFs with forms programatically. -extern crate lopdf; #[macro_use] extern crate bitflags; #[macro_use] extern crate derive_error; -use lopdf::{Document, ObjectId, Object, StringFormat}; -use std::path::Path; use std::collections::VecDeque; use std::io; +use std::io::Write; +use std::path::Path; use std::str; +use bitflags::_core::str::from_utf8; +use lopdf::{Document, Object, ObjectId, StringFormat}; + bitflags! { struct ButtonFlags: u32 { const NO_TOGGLE_TO_OFF = 0x8000; @@ -23,10 +24,10 @@ bitflags! { bitflags! { struct ChoiceFlags: u32 { - const COBMO = 0x40000; - const EDIT = 0x80000; - const SORT = 0x100000; - const MULTISELECT = 0x400000; + const COBMO = 0x20000; + const EDIT = 0x40000; + const SORT = 0x80000; + const MULTISELECT = 0x200000; const DO_NOT_SPELLCHECK = 0x800000; const COMMIT_ON_CHANGE = 0x8000000; } @@ -38,8 +39,8 @@ bitflags! { /// analyze the PDF and identify the fields. Then you can get and set the content of the fields by /// index. pub struct Form { - doc: Document, - form_ids: Vec + doc: Document, + form_ids: Vec, } /// The possible types of fillable form fields in a PDF @@ -50,7 +51,7 @@ pub enum FieldType { CheckBox, ListBox, ComboBox, - Text + Text, } /// The current state of a form field @@ -58,32 +59,39 @@ pub enum FieldType { pub enum FieldState { /// Push buttons have no state Button, - /// `selected` is the sigular option from `options` that is selected - Radio { selected: String, options: Vec }, + /// `selected` is the singular option from `options` that is selected + Radio { + selected: String, + options: Vec, + }, /// The toggle state of the checkbox CheckBox { is_checked: bool }, /// `selected` is the list of selected options from `options` - ListBox { selected: Vec, options: Vec, multiselect: bool }, + ListBox { + selected: Vec, + options: Vec, + multiselect: bool, + }, /// `selected` is the list of selected options from `options` - ComboBox { selected: Vec, options: Vec, multiselect: bool }, + ComboBox { + selected: Vec, + options: Vec, + editable: bool, + }, /// User Text Input - Text { text: String } + Text { text: String }, } #[derive(Debug, Error)] /// Errors that may occur while loading a PDF pub enum LoadError { - /// An IO Error - IoError(io::Error), - /// A dictionary key that must be present in order to find forms was not present - DictionaryKeyNotFound, + /// An Lopdf Error + LopdfError(lopdf::Error), /// The reference `ObjectId` did not point to any values #[error(non_std, no_from)] NoSuchReference(ObjectId), /// An element that was expected to be a reference was not a reference NotAReference, - /// A value that must be a certain type was not that type - UnexpectedType } /// Errors That may occur while setting values in a form @@ -94,7 +102,7 @@ pub enum ValueError { /// One or more selected values are not valid choices InvalidSelection, /// Multiple values were selected when only one was allowed - TooManySelected + TooManySelected, } trait PdfObjectDeref { @@ -105,13 +113,12 @@ 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) + _ => Err(LoadError::NotAReference), } } } impl Form { - /// Takes a reader containing a PDF with a fillable form, analyzes the content, and attempts to /// identify all of the fields the form has. pub fn load_from(reader: R) -> Result { @@ -126,24 +133,18 @@ impl Form { Self::load_doc(doc) } - fn load_doc(doc: Document) -> Result{ + fn load_doc(doc: Document) -> Result { let mut form_ids = Vec::new(); let mut queue = VecDeque::new(); // Block so borrow of doc ends before doc is moved into the result { // Get the form's top level fields - 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)? - // .deref(&doc)? - .as_array().ok_or(LoadError::UnexpectedType)?; + let catalog = doc.trailer.get(b"Root")?.deref(&doc)?.as_dict()?; + let acroform = catalog.get(b"AcroForm")?.deref(&doc)?.as_dict()?; + let fields_list = acroform + .get(b"Fields")? + // .deref(&doc)? + .as_array()?; queue.append(&mut VecDeque::from(fields_list.clone())); // Iterate over the fields @@ -151,11 +152,12 @@ impl Form { let obj = objref.deref(&doc)?; if let &Object::Dictionary(ref dict) = obj { // If the field has FT, it actually takes input. Save this - if let Some(_) = dict.get("FT") { + if let Ok(_) = dict.get(b"FT") { form_ids.push(objref.as_reference().unwrap()); } + // If this field has kids, they might have FT, so add them to the queue - if let Some(&Object::Array(ref kids)) = dict.get("Kids") { + if let Ok(&Object::Array(ref kids)) = dict.get(b"Kids") { queue.append(&mut VecDeque::from(kids.clone())); } } @@ -166,7 +168,7 @@ impl Form { /// Returns the number of fields the form has pub fn len(&self) -> usize { - self.form_ids.len() + self.form_ids.len() } /// Gets the type of field of the given index @@ -175,12 +177,20 @@ impl Form { /// This function will panic if the index is greater than the number of fields pub fn get_type(&self, n: usize) -> FieldType { // unwraps should be fine because load should have verified everything exists - let field = self.doc.objects.get(&self.form_ids[n]).unwrap().as_dict().unwrap(); + 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(); + let type_str = field.get(b"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) { + let flags = ButtonFlags::from_bits_truncate( + field.get(b"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 @@ -188,7 +198,9 @@ impl Form { 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); + let flags = ChoiceFlags::from_bits_truncate( + field.get(b"Ff").unwrap_or(&obj_zero).as_i64().unwrap() as u32, + ); if flags.intersects(ChoiceFlags::COBMO) { FieldType::ComboBox } else { @@ -199,12 +211,42 @@ impl Form { } } + /// Gets the name of field of the given index + /// + /// # Panics + /// This function will panic if the index is greater than the number of fields + pub fn get_name(&self, n: usize) -> Option { + // unwraps should be fine because load should have verified everything exists + let field = self + .doc + .objects + .get(&self.form_ids[n]) + .unwrap() + .as_dict() + .unwrap(); + + // The "T" key refers to the name of the field + match field.get(b"T") { + Ok(Object::String(data, _)) => String::from_utf8(data.clone()).ok(), + _ => None, + } + } + /// Gets the types of all of the fields in the form pub fn get_all_types(&self) -> Vec { let mut res = Vec::with_capacity(self.len()); for i in 0..self.len() { res.push(self.get_type(i)) - }; + } + res + } + + /// Gets the names of all of the fields in the form + pub fn get_all_names(&self) -> Vec> { + let mut res = Vec::with_capacity(self.len()); + for i in 0..self.len() { + res.push(self.get_name(i)) + } res } @@ -213,150 +255,223 @@ impl Form { /// # Panics /// This function will panic if the index is greater than the number of fields pub fn get_state(&self, n: usize) -> FieldState { - let field = self.doc.objects.get(&self.form_ids[n]).unwrap().as_dict().unwrap(); + 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() - } + selected: match field.get(b"V") { + Ok(name) => name.as_name_str().unwrap().to_owned(), + Err(_) => match field.get(b"AS") { + Ok(name) => name.as_name_str().unwrap().to_owned(), + Err(_) => "".to_owned(), + }, }, - options: self.get_possibilities(self.form_ids[n]) + 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::CheckBox => FieldState::CheckBox { + is_checked: match field.get(b"V") { + Ok(name) => { + if name.as_name_str().unwrap() == "Yes" { + true + } else { + false + } } - } + Err(_) => match field.get(b"AS") { + Ok(name) => { + if name.as_name_str().unwrap() == "Yes" { + true + } else { + false + } + } + Err(_) => false, + }, + }, }, FieldType::ListBox => FieldState::ListBox { // V field in a list box can be either text for one option, an array for many // options, or null - selected: match field.get("V") { - Some(selection) => match selection { - &Object::String(ref s,StringFormat::Literal) => vec![str::from_utf8(&s).unwrap().to_owned()], + selected: match field.get(b"V") { + Ok(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 { + if let &Object::String(ref s, StringFormat::Literal) = obj { res.push(str::from_utf8(&s).unwrap().to_owned()); } } res } - _ => Vec::new() + _ => Vec::new(), }, - None => Vec::new() + Err(_) => Vec::new(), }, // The options is an array of either text elements or arrays where the second // element is what we want - 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] { + options: match field.get(b"Opt") { + Ok(&Object::Array(ref options)) => options + .iter() + .map(|x| match x { + &Object::String(ref s, StringFormat::Literal) => { str::from_utf8(&s).unwrap().to_owned() - } else { - String::new() - }, - _ => String::new() - } - }).filter(|x| x.len() > 0).collect(), - _ => Vec::new() + } + &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().as_i64().unwrap() as u32); + let flags = ChoiceFlags::from_bits_truncate( + field + .get(b"Ff") + .unwrap_or(&Object::Integer(0)) + .as_i64() + .unwrap() as u32, + ); flags.intersects(ChoiceFlags::MULTISELECT) - } + }, }, FieldType::ComboBox => FieldState::ComboBox { // V field in a list box can be either text for one option, an array for many // options, or null - selected: match field.get("V") { - Some(selection) => match selection { - &Object::String(ref s,StringFormat::Literal) => vec![str::from_utf8(&s).unwrap().to_owned()], + selected: match field.get(b"V") { + Ok(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 { + if let &Object::String(ref s, StringFormat::Literal) = obj { res.push(str::from_utf8(&s).unwrap().to_owned()); } } res } - _ => Vec::new() + _ => Vec::new(), }, - None => Vec::new() + Err(_) => Vec::new(), }, // The options is an array of either text elements or arrays where the second // element is what we want - 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] { + options: match field.get(b"Opt") { + Ok(&Object::Array(ref options)) => options + .iter() + .map(|x| match x { + &Object::String(ref s, StringFormat::Literal) => { str::from_utf8(&s).unwrap().to_owned() - } else { - String::new() - }, - _ => String::new() - } - }).filter(|x| x.len() > 0).collect(), - _ => Vec::new() + } + &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(b"Ff") + .unwrap_or(&Object::Integer(0)) + .as_i64() + .unwrap() as u32, + ); + flags.intersects(ChoiceFlags::EDIT) + }, + }, + FieldType::Text => FieldState::Text { + text: match field.get(b"V") { + Ok(&Object::String(ref s, StringFormat::Literal)) => { + str::from_utf8(&s.clone()).unwrap().to_owned() + } + _ => "".to_owned(), }, - multiselect: { - - let flags = ChoiceFlags::from_bits_truncate(field.get("Ff").unwrap().as_i64().unwrap() as u32); - flags.intersects(ChoiceFlags::MULTISELECT) - } }, - 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() - } - - } } } - /// If the field at index `n` is a text field, fills in that field with the text `s`. /// If it is not a text field, returns ValueError /// /// # Panics /// Will panic if n is larger than the number of fields - pub fn set_text(&mut self, n: usize, s: String) -> Result<(),ValueError> { + 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"); + 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(b"AP"); Ok(()) - }, - _ => Err(ValueError::TypeMismatch) - + } + _ => Err(ValueError::TypeMismatch), } } fn get_possibilities(&self, oid: ObjectId) -> Vec { 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 kid in kids { - if let Some(&Object::Name(ref s)) = kid.deref(&self.doc).unwrap().as_dict().unwrap().get("AS") { - res.push(str::from_utf8(&s).unwrap().to_owned()); + let kids_obj = self + .doc + .objects + .get(&oid) + .unwrap() + .as_dict() + .unwrap() + .get(b"Kids"); + if let Ok(&Object::Array(ref kids)) = kids_obj { + for (i, kid) in kids.iter().enumerate() { + let mut found = false; + if let Ok(&Object::Dictionary(ref appearance_states)) = + kid.deref(&self.doc).unwrap().as_dict().unwrap().get(b"AP") + { + if let Ok(&Object::Dictionary(ref normal_appearance)) = + appearance_states.get(b"N") + { + for (key, _) in normal_appearance { + if key != b"Off" { + res.push(from_utf8(key).unwrap_or("").to_owned()); + found = true; + break; + } + } + } + } + + if !found { + res.push(i.to_string()); } } } + res } @@ -366,17 +481,32 @@ impl Form { /// /// # Panics /// Will panic if n is larger than the number of fields - pub fn set_check_box(&mut self, n: usize, is_checked: bool) -> Result<(),ValueError> { + 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); + 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) - + } + _ => Err(ValueError::TypeMismatch), } } @@ -386,53 +516,127 @@ impl Form { /// /// # Panics /// Will panic if n is larger than the number of fields - pub fn set_radio(&mut self, n: usize, choice: String) -> Result<(),ValueError> { + 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) - + 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), } } - /// If the field at index `n` is a listbox or comboox field, selects the options in `choice` - /// If it is not a listbox or combobox field or one of the choices is not a valid option, or if too many choices are selected, returns ValueError + /// If the field at index `n` is a listbox field, selects the options in `choice` + /// If it is not a listbox field or one of the choices is not a valid option, or if too many choices are selected, returns ValueError /// /// # Panics /// Will panic if n is larger than the number of fields - pub fn set_choice(&mut self, n: usize, choices: Vec) -> Result<(),ValueError> { + pub fn set_list_box(&mut self, n: usize, choices: Vec) -> Result<(), ValueError> { match self.get_state(n) { - FieldState::ListBox { selected: _, options, multiselect } | FieldState::ComboBox { selected: _, options, multiselect } => if choices.iter().fold(true, |a,h| options.contains(h) && a) { - if !multiselect && choices.len() > 1 { - Err(ValueError::TooManySelected) + 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 { - 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(()) + Err(ValueError::InvalidSelection) } - - } else { - Err(ValueError::InvalidSelection) - }, - _ => Err(ValueError::TypeMismatch) - + } + _ => Err(ValueError::TypeMismatch), } } + /// If the field at index `n` is a combobox field, selects the options in `choice` + /// If it is not a combobox field or one of the choices is not a valid option, or if too many choices are selected, returns ValueError + /// + /// # Panics + /// Will panic if n is larger than the number of fields + 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), + } + } /// Saves the form to the specified path - pub fn save>(&mut self, path: P) -> Result<(),io::Error> { + pub fn save>(&mut self, path: P) -> Result<(), io::Error> { self.doc.save(path).map(|_| ()) } + + /// Saves the form to the specified path + pub fn save_to(&mut self, target: &mut W) -> Result<(), io::Error> { + self.doc.save_to(target) + } } -- cgit v1.2.3 From 322e5e65353de1f746a599c8f08a1abefe8d3974 Mon Sep 17 00:00:00 2001 From: Emulator000 Date: Wed, 23 Sep 2020 23:12:08 +0200 Subject: Applied Clippy suggestions --- src/lib.rs | 79 ++++++++++++++++++++++++++++---------------------------------- 1 file changed, 36 insertions(+), 43 deletions(-) (limited to 'src') diff --git a/src/lib.rs b/src/lib.rs index 670f3ac..056a87f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -111,8 +111,8 @@ trait PdfObjectDeref { 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)), + match *self { + Object::Reference(oid) => doc.objects.get(&oid).ok_or(LoadError::NoSuchReference(oid)), _ => Err(LoadError::NotAReference), } } @@ -150,9 +150,9 @@ impl Form { // Iterate over the fields while let Some(objref) = queue.pop_front() { let obj = objref.deref(&doc)?; - if let &Object::Dictionary(ref dict) = obj { + if let Object::Dictionary(ref dict) = *obj { // If the field has FT, it actually takes input. Save this - if let Ok(_) = dict.get(b"FT") { + if dict.get(b"FT").is_ok() { form_ids.push(objref.as_reference().unwrap()); } @@ -171,6 +171,11 @@ impl Form { self.form_ids.len() } + /// Returns true if empty + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + /// Gets the type of field of the given index /// /// # Panics @@ -267,31 +272,19 @@ impl Form { FieldType::Radio => FieldState::Radio { selected: match field.get(b"V") { Ok(name) => name.as_name_str().unwrap().to_owned(), - Err(_) => match field.get(b"AS") { + _ => match field.get(b"AS") { Ok(name) => name.as_name_str().unwrap().to_owned(), - Err(_) => "".to_owned(), + _ => "".to_owned(), }, }, options: self.get_possibilities(self.form_ids[n]), }, FieldType::CheckBox => FieldState::CheckBox { is_checked: match field.get(b"V") { - Ok(name) => { - if name.as_name_str().unwrap() == "Yes" { - true - } else { - false - } - } - Err(_) => match field.get(b"AS") { - Ok(name) => { - if name.as_name_str().unwrap() == "Yes" { - true - } else { - false - } - } - Err(_) => false, + Ok(name) => name.as_name_str().unwrap() == "Yes", + _ => match field.get(b"AS") { + Ok(name) => name.as_name_str().unwrap() == "Yes", + _ => false, }, }, }, @@ -299,14 +292,14 @@ impl Form { // V field in a list box can be either text for one option, an array for many // options, or null selected: match field.get(b"V") { - Ok(selection) => match selection { - &Object::String(ref s, StringFormat::Literal) => { + Ok(selection) => match *selection { + Object::String(ref s, StringFormat::Literal) => { vec![str::from_utf8(&s).unwrap().to_owned()] } - &Object::Array(ref chosen) => { + Object::Array(ref chosen) => { let mut res = Vec::new(); for obj in chosen { - if let &Object::String(ref s, StringFormat::Literal) = obj { + if let Object::String(ref s, StringFormat::Literal) = *obj { res.push(str::from_utf8(&s).unwrap().to_owned()); } } @@ -314,19 +307,19 @@ impl Form { } _ => Vec::new(), }, - Err(_) => Vec::new(), + _ => Vec::new(), }, // The options is an array of either text elements or arrays where the second // element is what we want options: match field.get(b"Opt") { Ok(&Object::Array(ref options)) => options .iter() - .map(|x| match x { - &Object::String(ref s, StringFormat::Literal) => { + .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] { + Object::Array(ref arr) => { + if let Object::String(ref s, StringFormat::Literal) = &arr[1] { str::from_utf8(&s).unwrap().to_owned() } else { String::new() @@ -334,7 +327,7 @@ impl Form { } _ => String::new(), }) - .filter(|x| x.len() > 0) + .filter(|x| !x.is_empty()) .collect(), _ => Vec::new(), }, @@ -353,14 +346,14 @@ impl Form { // V field in a list box can be either text for one option, an array for many // options, or null selected: match field.get(b"V") { - Ok(selection) => match selection { - &Object::String(ref s, StringFormat::Literal) => { + Ok(selection) => match *selection { + Object::String(ref s, StringFormat::Literal) => { vec![str::from_utf8(&s).unwrap().to_owned()] } - &Object::Array(ref chosen) => { + Object::Array(ref chosen) => { let mut res = Vec::new(); for obj in chosen { - if let &Object::String(ref s, StringFormat::Literal) = obj { + if let Object::String(ref s, StringFormat::Literal) = *obj { res.push(str::from_utf8(&s).unwrap().to_owned()); } } @@ -368,19 +361,19 @@ impl Form { } _ => Vec::new(), }, - Err(_) => Vec::new(), + _ => Vec::new(), }, // The options is an array of either text elements or arrays where the second // element is what we want options: match field.get(b"Opt") { Ok(&Object::Array(ref options)) => options .iter() - .map(|x| match x { - &Object::String(ref s, StringFormat::Literal) => { + .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] { + Object::Array(ref arr) => { + if let Object::String(ref s, StringFormat::Literal) = &arr[1] { str::from_utf8(&s).unwrap().to_owned() } else { String::new() @@ -388,7 +381,7 @@ impl Form { } _ => String::new(), }) - .filter(|x| x.len() > 0) + .filter(|x| !x.is_empty()) .collect(), _ => Vec::new(), }, @@ -619,7 +612,7 @@ impl Form { .unwrap(); field.set( "V", - Object::String(choice.clone().into_bytes(), StringFormat::Literal), + Object::String(choice.into_bytes(), StringFormat::Literal), ); Ok(()) } else { -- cgit v1.2.3 From bc98ee497b9082aeec64415fc2e94eee442819d9 Mon Sep 17 00:00:00 2001 From: Emulator000 Date: Wed, 23 Sep 2020 23:48:35 +0200 Subject: Some field flags utility and moved some structs to utils module, added "required" and "readonly" properties for some field state and fixed some structs --- src/lib.rs | 225 +++++++++++++++++++++++++++++------------------------------ src/utils.rs | 49 +++++++++++++ 2 files changed, 158 insertions(+), 116 deletions(-) create mode 100644 src/utils.rs (limited to 'src') diff --git a/src/lib.rs b/src/lib.rs index 056a87f..d786957 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,8 @@ extern crate bitflags; #[macro_use] extern crate derive_error; +mod utils; + use std::collections::VecDeque; use std::io; use std::io::Write; @@ -10,28 +12,10 @@ use std::path::Path; use std::str; use bitflags::_core::str::from_utf8; -use lopdf::{Document, Object, ObjectId, StringFormat}; - -bitflags! { - struct ButtonFlags: u32 { - const NO_TOGGLE_TO_OFF = 0x8000; - const RADIO = 0x10000; - const PUSHBUTTON = 0x20000; - const RADIO_IN_UNISON = 0x4000000; - } -} +use lopdf::{Document, Object, ObjectId, StringFormat}; -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; - } -} +use crate::utils::*; /// A PDF Form that contains fillable fields /// @@ -54,6 +38,30 @@ pub enum FieldType { Text, } +#[derive(Debug, Error)] +/// Errors that may occur while loading a PDF +pub enum LoadError { + /// An Lopdf Error + LopdfError(lopdf::Error), + /// The reference `ObjectId` did not point to any values + #[error(non_std, no_from)] + NoSuchReference(ObjectId), + /// An element that was expected to be a reference was not a reference + NotAReference, +} + +/// Errors That may occur while setting values in a form +#[derive(Debug, Error)] +pub enum ValueError { + /// The method used to set the state is incompatible with the type of the field + TypeMismatch, + /// One or more selected values are not valid choices + InvalidSelection, + /// Multiple values were selected when only one was allowed + TooManySelected, + /// Readonly field cannot be edited + Readonly, +} /// The current state of a form field #[derive(Debug)] pub enum FieldState { @@ -63,46 +71,37 @@ pub enum FieldState { Radio { selected: String, options: Vec, + readonly: bool, + required: bool, }, /// The toggle state of the checkbox - CheckBox { is_checked: bool }, + CheckBox { + is_checked: bool, + readonly: bool, + required: bool, + }, /// `selected` is the list of selected options from `options` ListBox { selected: Vec, options: Vec, multiselect: bool, + readonly: bool, + required: bool, }, /// `selected` is the list of selected options from `options` ComboBox { selected: Vec, options: Vec, editable: bool, + readonly: bool, + required: bool, }, /// User Text Input - Text { text: String }, -} - -#[derive(Debug, Error)] -/// Errors that may occur while loading a PDF -pub enum LoadError { - /// An Lopdf Error - LopdfError(lopdf::Error), - /// The reference `ObjectId` did not point to any values - #[error(non_std, no_from)] - NoSuchReference(ObjectId), - /// An element that was expected to be a reference was not a reference - NotAReference, -} - -/// Errors That may occur while setting values in a form -#[derive(Debug, Error)] -pub enum ValueError { - /// The method used to set the state is incompatible with the type of the field - TypeMismatch, - /// One or more selected values are not valid choices - InvalidSelection, - /// Multiple values were selected when only one was allowed - TooManySelected, + Text { + text: String, + readonly: bool, + required: bool, + }, } trait PdfObjectDeref { @@ -189,12 +188,10 @@ impl Form { .unwrap() .as_dict() .unwrap(); - let obj_zero = Object::Integer(0); + let type_str = field.get(b"FT").unwrap().as_name_str().unwrap(); if type_str == "Btn" { - let flags = ButtonFlags::from_bits_truncate( - field.get(b"Ff").unwrap_or(&obj_zero).as_i64().unwrap() as u32, - ); + let flags = ButtonFlags::from_bits_truncate(get_field_flags(field)); if flags.intersects(ButtonFlags::RADIO | ButtonFlags::NO_TOGGLE_TO_OFF) { FieldType::Radio } else if flags.intersects(ButtonFlags::PUSHBUTTON) { @@ -203,9 +200,7 @@ impl Form { FieldType::CheckBox } } else if type_str == "Ch" { - let flags = ChoiceFlags::from_bits_truncate( - field.get(b"Ff").unwrap_or(&obj_zero).as_i64().unwrap() as u32, - ); + let flags = ChoiceFlags::from_bits_truncate(get_field_flags(field)); if flags.intersects(ChoiceFlags::COBMO) { FieldType::ComboBox } else { @@ -278,6 +273,8 @@ impl Form { }, }, options: self.get_possibilities(self.form_ids[n]), + readonly: is_read_only(field), + required: is_required(field), }, FieldType::CheckBox => FieldState::CheckBox { is_checked: match field.get(b"V") { @@ -287,6 +284,8 @@ impl Form { _ => false, }, }, + readonly: is_read_only(field), + required: is_required(field), }, FieldType::ListBox => FieldState::ListBox { // V field in a list box can be either text for one option, an array for many @@ -332,15 +331,11 @@ impl Form { _ => Vec::new(), }, multiselect: { - let flags = ChoiceFlags::from_bits_truncate( - field - .get(b"Ff") - .unwrap_or(&Object::Integer(0)) - .as_i64() - .unwrap() as u32, - ); + let flags = ChoiceFlags::from_bits_truncate(get_field_flags(field)); flags.intersects(ChoiceFlags::MULTISELECT) }, + readonly: is_read_only(field), + required: is_required(field), }, FieldType::ComboBox => FieldState::ComboBox { // V field in a list box can be either text for one option, an array for many @@ -386,15 +381,12 @@ impl Form { _ => Vec::new(), }, editable: { - let flags = ChoiceFlags::from_bits_truncate( - field - .get(b"Ff") - .unwrap_or(&Object::Integer(0)) - .as_i64() - .unwrap() as u32, - ); + let flags = ChoiceFlags::from_bits_truncate(get_field_flags(field)); + flags.intersects(ChoiceFlags::EDIT) }, + readonly: is_read_only(field), + required: is_required(field), }, FieldType::Text => FieldState::Text { text: match field.get(b"V") { @@ -403,6 +395,8 @@ impl Form { } _ => "".to_owned(), }, + readonly: is_read_only(field), + required: is_required(field), }, } } @@ -413,8 +407,8 @@ impl Form { /// # Panics /// Will panic if n is larger than the number of fields pub fn set_text(&mut self, n: usize, s: String) -> Result<(), ValueError> { - match self.get_type(n) { - FieldType::Text => { + match self.get_state(n) { + FieldState::Text { .. } => { let field = self .doc .objects @@ -422,52 +416,16 @@ impl Form { .unwrap() .as_dict_mut() .unwrap(); + field.set("V", Object::String(s.into_bytes(), StringFormat::Literal)); field.remove(b"AP"); + Ok(()) } _ => Err(ValueError::TypeMismatch), } } - fn get_possibilities(&self, oid: ObjectId) -> Vec { - let mut res = Vec::new(); - let kids_obj = self - .doc - .objects - .get(&oid) - .unwrap() - .as_dict() - .unwrap() - .get(b"Kids"); - if let Ok(&Object::Array(ref kids)) = kids_obj { - for (i, kid) in kids.iter().enumerate() { - let mut found = false; - if let Ok(&Object::Dictionary(ref appearance_states)) = - kid.deref(&self.doc).unwrap().as_dict().unwrap().get(b"AP") - { - if let Ok(&Object::Dictionary(ref normal_appearance)) = - appearance_states.get(b"N") - { - for (key, _) in normal_appearance { - if key != b"Off" { - res.push(from_utf8(key).unwrap_or("").to_owned()); - found = true; - break; - } - } - } - } - - if !found { - res.push(i.to_string()); - } - } - } - - res - } - /// If the field at index `n` is a checkbox field, toggles the check box based on the value /// `is_checked`. /// If it is not a checkbox field, returns ValueError @@ -475,8 +433,8 @@ impl Form { /// # Panics /// Will panic if n is larger than the number of fields pub fn set_check_box(&mut self, n: usize, is_checked: bool) -> Result<(), ValueError> { - match self.get_type(n) { - FieldType::CheckBox => { + match self.get_state(n) { + FieldState::CheckBox { .. } => { let state = Object::Name( { if is_checked { @@ -495,8 +453,10 @@ impl Form { .unwrap() .as_dict_mut() .unwrap(); + field.set("V", state.clone()); field.set("AS", state); + Ok(()) } _ => Err(ValueError::TypeMismatch), @@ -511,10 +471,7 @@ impl Form { /// Will panic if n is larger than the number of fields pub fn set_radio(&mut self, n: usize, choice: String) -> Result<(), ValueError> { match self.get_state(n) { - FieldState::Radio { - selected: _, - options, - } => { + FieldState::Radio { options, .. } => { if options.contains(&choice) { let field = self .doc @@ -541,9 +498,9 @@ impl Form { pub fn set_list_box(&mut self, n: usize, choices: Vec) -> 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 { @@ -598,9 +555,7 @@ impl Form { pub fn set_combo_box(&mut self, n: usize, choice: String) -> Result<(), ValueError> { match self.get_state(n) { FieldState::ComboBox { - selected: _, - options, - editable, + options, editable, .. } => { if options.contains(&choice) || editable { let field = self @@ -632,4 +587,42 @@ impl Form { pub fn save_to(&mut self, target: &mut W) -> Result<(), io::Error> { self.doc.save_to(target) } + + fn get_possibilities(&self, oid: ObjectId) -> Vec { + let mut res = Vec::new(); + let kids_obj = self + .doc + .objects + .get(&oid) + .unwrap() + .as_dict() + .unwrap() + .get(b"Kids"); + if let Ok(&Object::Array(ref kids)) = kids_obj { + for (i, kid) in kids.iter().enumerate() { + let mut found = false; + if let Ok(&Object::Dictionary(ref appearance_states)) = + kid.deref(&self.doc).unwrap().as_dict().unwrap().get(b"AP") + { + if let Ok(&Object::Dictionary(ref normal_appearance)) = + appearance_states.get(b"N") + { + for (key, _) in normal_appearance { + if key != b"Off" { + res.push(from_utf8(key).unwrap_or("").to_owned()); + found = true; + break; + } + } + } + } + + if !found { + res.push(i.to_string()); + } + } + } + + res + } } diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..7ff4e41 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,49 @@ +use lopdf::{Dictionary, Object}; + +bitflags! { + pub struct FieldFlags: u32 { + const READONLY = 0x1; + const REQUIRED = 0x2; + } +} + +bitflags! { + pub struct ButtonFlags: u32 { + const NO_TOGGLE_TO_OFF = 0x8000; + const RADIO = 0x10000; + const PUSHBUTTON = 0x20000; + const RADIO_IN_UNISON = 0x4000000; + + } +} + +bitflags! { + pub 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 fn is_read_only(field: &Dictionary) -> bool { + let flags = FieldFlags::from_bits_truncate(get_field_flags(field)); + + flags.intersects(FieldFlags::READONLY) +} + +pub fn is_required(field: &Dictionary) -> bool { + let flags = FieldFlags::from_bits_truncate(get_field_flags(field)); + + flags.intersects(FieldFlags::REQUIRED) +} + +pub fn get_field_flags(field: &Dictionary) -> u32 { + field + .get(b"Ff") + .unwrap_or(&Object::Integer(0)) + .as_i64() + .unwrap() as u32 +} -- cgit v1.2.3 From 895a753cd531fc2ad43365299775910c2ce04efa Mon Sep 17 00:00:00 2001 From: Emulator000 Date: Thu, 24 Sep 2020 00:35:10 +0200 Subject: Handling of unknown fields --- src/lib.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/lib.rs b/src/lib.rs index d786957..59c36f9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,6 +36,7 @@ pub enum FieldType { ListBox, ComboBox, Text, + Unknown, } #[derive(Debug, Error)] @@ -102,6 +103,8 @@ pub enum FieldState { readonly: bool, required: bool, }, + /// Unknown fields have no state + Unknown, } trait PdfObjectDeref { @@ -206,8 +209,10 @@ impl Form { } else { FieldType::ListBox } - } else { + } else if type_str == "Tx" { FieldType::Text + } else { + FieldType::Unknown } } @@ -398,6 +403,7 @@ impl Form { readonly: is_read_only(field), required: is_required(field), }, + FieldType::Unknown => FieldState::Unknown, } } -- cgit v1.2.3