Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Form Field types (PDF 1.7 Section 12.7.4) #23

Merged
merged 7 commits into from
Oct 22, 2023
Merged
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
339 changes: 339 additions & 0 deletions src/forms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub struct Field<'a> {

writer!(Field: |obj| Self { dict: obj.dict() });

/// Permissible on all fields.
impl<'a> Field<'a> {
/// Write the `/FT` attribute to set the type of this field.
pub fn field_type(&mut self, typ: FieldType) -> &mut Self {
Expand Down Expand Up @@ -76,8 +77,211 @@ impl<'a> Field<'a> {
}
}

/// Only permissible on text fields.
impl<'a> Field<'a> {
// TODO: the spec likely means the equivalent of unicode graphemes here
// for characters

/// Write the `/MaxLen` attribute to set the maximum length of the field's
/// text in characters. Only permissible on text fields.
pub fn text_max_len(&mut self, len: i32) -> &mut Self {
self.dict.pair(Name(b"MaxLen"), len);
self
}

/// Write the `/V` attribute to set the value of this text field.
/// Only permissible on text fields.
pub fn text_value(&mut self, value: TextStr) -> &mut Self {
self.dict.pair(Name(b"V"), value);
self
}

/// Start writing the `/DV` attribute to set the default value of this text
/// field. Only permissible on text fields.
pub fn text_default_value(&mut self, value: TextStr) -> &mut Self {
self.dict.pair(Name(b"DV"), value);
self
}
reknih marked this conversation as resolved.
Show resolved Hide resolved
}

/// Only permissible on fields containing variable text.
impl<'a> Field<'a> {
/// Write the `/DA` attribute containing a sequence of valid page-content
/// graphics or text state operators that define such properties as the
/// field's text size and colour. Only permissible on fields containing
/// variable text.
pub fn vartext_default_appearance(&mut self, appearance: Str) -> &mut Self {
self.dict.pair(Name(b"DA"), appearance);
self
}
reknih marked this conversation as resolved.
Show resolved Hide resolved

/// Write the `/Q` attribute to set the quadding (justification) that shall
/// be used in dispalying the text. Only permissible on fields containing
/// variable text.
pub fn vartext_quadding(&mut self, quadding: Quadding) -> &mut Self {
self.dict.pair(Name(b"Q"), quadding as u32 as i32);
tingerrr marked this conversation as resolved.
Show resolved Hide resolved
self
}
tingerrr marked this conversation as resolved.
Show resolved Hide resolved

/// Write the `/DS` attribute to set the default style string. Only
/// permissible on fields containing variable text. PDF 1.5+.
pub fn vartext_default_style(&mut self, style: TextStr) -> &mut Self {
self.dict.pair(Name(b"DS"), style);
self
}

/// Write the `/RV` attribute to set the value of this variable text field.
/// Only permissible on fields containing variable text. PDF 1.5+.
pub fn vartext_rich_value(&mut self, value: TextStr) -> &mut Self {
self.dict.pair(Name(b"RV"), value);
self
}
}

/// Only permissible on choice fields.
impl<'a> Field<'a> {
/// Start writing the `/Opt` array to set the options that shall be
/// presented to the user.
pub fn choice_options(&mut self) -> ChoiceOptions<'_> {
self.dict.insert(Name(b"Opt")).start()
}

/// Write the `/TI` attribute to set the index in the
/// [`Field::choice_options`] array of the first visible option for
/// scrollable lists.
pub fn choice_top_index(&mut self, index: i32) -> &mut Self {
self.dict.pair(Name(b"TI"), index);
self
}

/// Start writing the `/I` array to set the indices of the currently
/// selected options. The integers in this array must be sorted in ascending
/// order and correspond to 0-based indices in the [`Field::choice_options`]
/// array.
///
/// This entry shall be used for choice fields which allow multiple
/// selections ([`FieldFlags::MULTI_SELECT`]). This means when two or more
/// elements in the [`Field::choice_options`] array have different names
/// but export the same value or when the value fo the choice field is an
/// array. This entry should not be used for choice fields that do not allow
/// multiple selections. PDF 1.4+.
pub fn choice_indices(&mut self) -> TypedArray<'_, i32> {
self.dict.insert(Name(b"I")).array().typed()
}

/// Write the `/V` attribute to set the currently selected values
/// of this choice field. Should be one of the values given in
/// [`Self::choice_options`] or `None` if no choice is selected. Only
/// permissible on choice fields.
pub fn choice_value<'b>(&mut self, option: Option<TextStr>) -> &mut Self {
match option {
Some(value) => self.dict.pair(Name(b"V"), value),
None => self.dict.pair(Name(b"V"), Null),
};
self
}

/// Write the `/V` attribute to set the currently selected values of this
/// choice field. See also [`Self::choice_value`], for a single or no value.
/// Only permissible on choice fields.
pub fn choice_values<'b>(
&mut self,
options: impl IntoIterator<Item = TextStr<'b>>,
) -> &mut Self {
self.dict.insert(Name(b"V")).array().items(options);
self
}

/// Write the `/DV` attribute to set the default selected value
/// of this choice field. Should be one of the values given in
/// [`Self::choice_options`] or `None` if no choice is selected. Only
/// permissible on choice fields.
pub fn choice_default_value(&mut self, option: Option<TextStr>) -> &mut Self {
match option {
Some(value) => self.dict.pair(Name(b"DV"), value),
None => self.dict.pair(Name(b"DV"), Null),
};
self
}

/// Write the `/DV` attribute to set the default selected values of this
/// choice field. See also [`Self::choice_default_value`], for a single or
/// no value. Only permissible on choice fields.
pub fn choice_default_values<'b>(
&mut self,
options: impl IntoIterator<Item = TextStr<'b>>,
) -> &mut Self {
self.dict.insert(Name(b"DV")).array().items(options);
self
}
}

/// Only permissible on button fields.
tingerrr marked this conversation as resolved.
Show resolved Hide resolved
impl<'a> Field<'a> {
/// Start writing the `/Opt` array to set the export values of children of
/// this field. Only permissible on checkbox fields, or radio button fields.
/// PDF 1.4+.
pub fn button_options(&mut self) -> TypedArray<'_, TextStr> {
self.dict.insert(Name(b"Opt")).array().typed()
}
}

/// Only permissible on check box fields.
impl<'a> Field<'a> {
/// Write the `/V` attribute to set the state of this check box field.
/// The state corresponds to an appearance stream in the
/// [appearance dictionary](Appearance) of this field's widget
/// [annotation](Annotation). Only permissible on check box fields.
pub fn checkbox_value(&mut self, state: CheckBoxState) -> &mut Self {
self.dict.pair(Name(b"V"), state.to_name());
self
}

/// Write the `/DV` attribute to set the default state of this check box
/// field. The state corresponds to an appearance stream in the
/// [appearance dictionary](Appearance) of this field's widget
/// [annotation](Annotation). Only permissible on check box fields.
pub fn checkbox_default_value(&mut self, state: CheckBoxState) -> &mut Self {
self.dict.pair(Name(b"DV"), state.to_name());
self
}
}

/// Only permissible on radio button fields.
impl<'a> Field<'a> {
/// Write the `/V` attribute to set the state of this check box field.
/// The state corresponds to an appearance stream in the
/// [appearance dictionary](Appearance) of this field's widget
/// [annotation](Annotation). Only permissible on radio button fields.
pub fn radio_value(&mut self, state: RadioState) -> &mut Self {
self.dict.pair(Name(b"V"), state.to_name());
self
}

/// Write the `/DV` attribute to set the default state of this check box
/// field. The state corresponds to an appearance stream in the
/// [appearance dictionary](Appearance) of this field's widget
/// [annotation](Annotation). Only permissible on radio button fields.
pub fn radio_default_value(&mut self, state: RadioState) -> &mut Self {
self.dict.pair(Name(b"DV"), state.to_name());
self
}
}

deref!('a, Field<'a> => Dict<'a>, dict);

/// The quadding (justification) of a field containing variable text.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
#[repr(u32)]
pub enum Quadding {
/// Left justify the text.
Left = 0,
/// Center justify the text.
Center = 1,
/// Right justify the text.
Right = 2,
}

/// The type of a [`Field`].
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum FieldType {
Expand All @@ -104,6 +308,40 @@ impl FieldType {
}
}

/// The state of a check box [`Field`].
pub enum CheckBoxState {
/// The check box selected state `/Yes`.
Yes,
/// The check box unselected state `/Off`.
Off,
}

impl CheckBoxState {
pub(crate) fn to_name(self) -> Name<'static> {
match self {
Self::Yes => Name(b"Yes"),
Self::Off => Name(b"Off"),
}
}
}

/// The state of a radio button [`Field`].
pub enum RadioState<'a> {
/// The radio button with the given name is selected.
Selected(Name<'a>),
/// No radio button is selected `/Off`.
Off,
}

impl<'a> RadioState<'a> {
pub(crate) fn to_name(self) -> Name<'a> {
match self {
Self::Selected(name) => name,
Self::Off => Name(b"Off"),
}
}
}

bitflags::bitflags! {
/// Bitflags describing various characteristics of a form field.
pub struct FieldFlags: u32 {
Expand All @@ -119,5 +357,106 @@ bitflags::bitflags! {
/// The field shall not be exported by a
/// [submit-form](crate::types::ActionType::SubmitForm)[`Action`].
const NO_EXPORT = 1 << 3;

// button specific flags
tingerrr marked this conversation as resolved.
Show resolved Hide resolved

/// Exactly one radio button shall be selected at all times; selecting
/// the currently selected button has no effect. If unset, clicking
/// the selected button deselects it, leaving no button selected. Only
/// permissible for radio buttons.
const NO_TOGGLE_TO_OFF = 1 << 15;
/// The field is a set of radio buttons; if clear, the field is a check
/// box. This flag may be set only if the `PUSHBUTTON` flag is unset.
const RADIO = 1 << 16;
/// The field is a push button that does not retain a permanent
/// value.
const PUSHBUTTON = 1 << 17;
/// A group of radio buttons within a radio button field that use the
/// same value for the on state will turn on and off in unison; that
/// is if one is checked, they are all checked. If unset, the buttons
/// are mutually exclusive (the same behavior as HTML radio buttons).
/// PDF 1.5+.
const RADIOS_IN_UNISON = 1 << 26;

// text field specific flags
tingerrr marked this conversation as resolved.
Show resolved Hide resolved

/// The text may contain multiple lines of text, otherwise the text is
/// restricted to one line.
const MULTILINE = 1 << 13;
/// The text contains a password and should not be echoed visibly to
/// the screen.
const PASSWORD = 1 << 14;
/// The entered text represents a path to a file who's contents shall be
/// submitted as the value of the field. PDF 1.4+.
const FILE_SELECT = 1 << 21;
/// The entered text shall not be spell-checked, can be used for text
/// and choice fields.
const DO_NOT_SPELL_CHECK = 1 << 23;
tingerrr marked this conversation as resolved.
Show resolved Hide resolved
/// The field shall not scroll horizontally (for single-line) or
/// vertically (for multi-line) to accomodate more text. Once the field
/// is full, no further text shall be accepted for interactive form
/// filling; for non-interactive form filling, the filler should take
/// care not to add more character than will visibly fit in the defined
/// area. PDF 1.4+.
const DO_NOT_SCROLL = 1 << 24;
/// The field shall eb automatically divided into as many equally
/// spaced postions or _combs_ as the value of [`Field::max_len`]
/// and the text is layed out into these combs. May only be set if
/// the [`Field::max_len`] property is set and if the [`MULTILINE`],
/// [`PASSWORD`] and [`FILE_SELECT`] flags are clear. PDF 1.5+.
const COMB = 1 << 25;
/// The value of this field shall be a rich text string. If the field
/// has a value, the [`TextField::rich_text_value`] shall specify the
/// rich text string. PDF 1.5+.
const RICH_TEXT = 1 << 26;

// choice field specific flags
tingerrr marked this conversation as resolved.
Show resolved Hide resolved

/// The field is a combo box if set, else it's a list box.
const COMBO = 1 << 18;
/// The combo box shall include an editable text box as well as a
/// drop-down list. Shall only be used if [`COMBO`] is set.
const EDIT = 1 << 19;
/// The field’s option items shall be sorted alphabetically. This
/// flag is intended for use by writers, not by readers.
const SORT = 1 << 20;
/// More than one option of the choice field may be selected
/// simultaneously. PDF 1.4+.
const MULTI_SELECT = 1 << 22;
/// The new value shall be committed as soon as a selection is made
/// (commonly with the mouse). In this case, supplying a value for
/// a field involves three actions: selecting the field for fill-in,
/// selecting a choice for the fill-in value, and leaving that field,
/// which finalizes or "commits" the data choice and triggers any
/// actions associated with the entry or changing of this data.
///
/// If set, processing does not wait for leaving the field action to
/// occur, but immediately proceeds to the third step. PDF 1.5+.
const COMMIT_ON_SEL_CHANGE = 1 << 27;
}
}

/// Writer for a _choice options array_.
///
/// This struct is created by [`Field::choice_options`].
pub struct ChoiceOptions<'a> {
array: Array<'a>,
}

writer!(ChoiceOptions: |obj| Self { array: obj.array() });

impl<'a> ChoiceOptions<'a> {
/// Add an option with the given value.
pub fn option(&mut self, value: TextStr) -> &mut Self {
self.array.item(value);
self
}

/// Add an option with the given value and export value.
pub fn export(&mut self, value: TextStr, export_value: TextStr) -> &mut Self {
self.array.push().array().items([export_value, value]);
self
}
}

deref!('a, ChoiceOptions<'a> => Array<'a>, array);
tingerrr marked this conversation as resolved.
Show resolved Hide resolved