From f4a55a919c57d8982c31b55211b8ce66af5ddb17 Mon Sep 17 00:00:00 2001 From: tingerrr Date: Tue, 30 Apr 2024 16:09:40 +0200 Subject: [PATCH] Feature: Interactive Form Dictionary (PDF 1.7 Section 12.7.2) & Appearance Streams (12.5.5) (#25) --- examples/forms.rs | 281 +++++++++++++++++++++++++++++++++++++++++++ src/actions.rs | 37 ++++-- src/annotations.rs | 4 +- src/forms.rs | 292 ++++++++++++++++++++++++++++++--------------- src/lib.rs | 4 +- src/structure.rs | 6 + 6 files changed, 516 insertions(+), 108 deletions(-) create mode 100644 examples/forms.rs diff --git a/examples/forms.rs b/examples/forms.rs new file mode 100644 index 0000000..f720972 --- /dev/null +++ b/examples/forms.rs @@ -0,0 +1,281 @@ +//! This example shows how to create forms accepted by the most popular readers. + +use pdf_writer::types::{ + ActionType, AnnotationFlags, BorderType, FieldFlags, FieldType, FormActionFlags, +}; +use pdf_writer::{Content, Finish, Name, Pdf, Rect, Ref, Str, TextStr}; + +fn main() -> std::io::Result<()> { + let mut pdf = Pdf::new(); + + // Let's set up our primary font, we'll have to reference it a few times. + let text_font_id = Ref::new(1); + let text_font_name = Name(b"F1"); + + // Here we'll set up our Dingbat font, this is used for symbols such as the + // ticks in checkboxes. + let symbol_font_id = Ref::new(2); + let symbol_font_name = Name(b"F2"); + + // One of the most common form field types is the text field. Let's add that + // and look at some of the basics of PDF form fields. + let text_field_id = Ref::new(4); + + // We start by writing a form field dictionary with an id which we later + // need for referencing it. + let mut field = pdf.form_field(text_field_id); + + // While the `/T` attribute is optional according to the spec, you should + // include it, most readers will only render widget annotations with both + // partial name and field type. Next, we set it's value and default value: + // - The value is used to store what the user has put into the field. + // - The default value is used when resetting the form. + field + .partial_name(TextStr("text")) + .field_type(FieldType::Text) + .text_value(TextStr("Hello")) + .text_default_value(TextStr("Who reset me")); + + // Our field is a terminal field because it has no children, so it's merged + // with its widget annotation. The widget annotation is what declares the + // appearance and position in the document, whereas the field defines its + // semantic behavior for the document-wide form. The appearance is more + // relevant to button fields, we'll see how to cofigure it below. + let mut annot = field.into_annotation(); + annot.rect(Rect::new(108.0, 730.0, 208.0, 748.0)); + + // We can pass some fairly simple appearances here, common things such + // as the border color and style. This will give out field a purple + // underline, keep in mind that this may be drowned out by the viewer's + // form highlighting. + annot.border_style().style(BorderType::Underline); + annot.appearance_characteristics().border_color_rgb(0.0, 0.0, 0.5); + + // The reader will usually provide a default appearance and automatically + // highlight form fields. The appearance is relevant for printing however. + // While we don't provide an explicit appearnce here, if we did we likely + // want this flag to be set. + annot.flags(AnnotationFlags::PRINT); + annot.finish(); + + // A good form has radio buttons. Radio buttons are checkboxes which turn + // off when another checkbox is turned on. A group of radio button widget + // annotations shares a single radio button field as parent. + let radio_group_id = Ref::new(5); + + // The FormXObjects for our checkboxes need bounding boxes, in this case + // these are the same size as out rectangles, but within their coordinate + // system. + let bbox = Rect::new(0.0, 0.0, 30.0, 18.0); + + // We define our three radio buttons, they all have a different appearance + // streams, but if they shared the same appearance stream and used the + // RADIOS_IN_UNISON flag, then two buttons could refer to the same choice. + // This is not widely supported, so we'll simply showcase some normal radio + // buttons here. + // + // NOTE: A reader like Okular will also use on-state name in the default + // appearance. + let radios = [ + (Ref::new(6), Rect::new(108.0, 710.0, 138.0, 728.0), b"ch1"), + (Ref::new(7), Rect::new(140.0, 710.0, 170.0, 728.0), b"ch2"), + (Ref::new(8), Rect::new(172.0, 710.0, 202.0, 728.0), b"ch3"), + ]; + // First, we define the radio group parent. The children of this field will + // be our actual buttons. We can define most of the radio related properties + // here. + let mut field = pdf.form_field(radio_group_id); + + // We set some flags to get the exact behavior we want. + // - FieldFlags::NO_TOGGLE_OFF means that once a button is selected it + // cannot be manually turned off without turning another button on. + // - FieldFlags::RADIOS_IN_UNISON ensures that if we have buttons which use + // the same appearance on-state, they'll be toggled in unison with the + // others (although we don't use this here). + // Finally we define the children of this field, the widget annotations + // which again define appearance and postion of the individual buttons. + // + // NOTE: by the time of writing this, RADIOS_IN_UNISON does not work + // correctly pdf.js (firefox), okular or evince. + field + .partial_name(TextStr("radio")) + .field_type(FieldType::Button) + .field_flags( + FieldFlags::RADIO + | FieldFlags::NO_TOGGLE_TO_OFF + | FieldFlags::RADIOS_IN_UNISON, + ) + .children(radios.map(|(id, _, _)| id)); + field.finish(); + + // For buttons appearances are more relevant when printing as they're + // usually not as easy to find as text fields if they have no appearance. + let radio_on_appearance_id = Ref::new(9); + let radio_off_appearance_id = Ref::new(10); + + // Here we prepare our appearances, the on appearance is a tick and the off + // appearance is empty. + let mut content = Content::new(); + content.save_state(); + content.begin_text(); + content.set_fill_gray(0.0); + content.set_font(symbol_font_name, 14.0); + // The character 4 is a tick in this font. + content.show(Str(b"4")); + content.end_text(); + content.restore_state(); + + let on_stream = content.finish(); + let mut on_appearance = pdf.form_xobject(radio_on_appearance_id, &on_stream); + + on_appearance.bbox(bbox); + + // We use the symbol font to display the tick, so we need to add it to the + // resources of the appearance stream. + on_appearance + .resources() + .fonts() + .pair(symbol_font_name, symbol_font_id); + + on_appearance.finish(); + + // Our off appearance is empty, we haven't ticked the box. + pdf.form_xobject(radio_off_appearance_id, &Content::new().finish()) + .bbox(bbox); + + // Now we'll write a widget annotation for each button. + for (id, rect, state) in radios { + // While we create a field here we could directly create widget + // annotation too. + let mut field = pdf.form_field(id); + + // Each button shares the single parent. + field.parent(radio_group_id); + + let mut annot = field.into_annotation(); + annot.rect(rect).flags(AnnotationFlags::PRINT); + + // This is the state the button starts off with. `/Off` is the off state + // and is the same for all radio buttons. The `on` state gets its own + // name to distinguish different buttons. + annot.appearance_state(Name(b"Off")); + + // Finally we set the appearance dictionary to contain a normal + // appearance sub dictionary mapping both on and off state to the + // respective FormXObject. + { + let mut appearance = annot.appearance(); + appearance.normal().streams().pairs([ + (Name(state), radio_on_appearance_id), + (Name(b"Off"), radio_off_appearance_id), + ]); + } + } + + // Let's add a dropdown menu and allow the user to chose from preconfigrued + // options while allowing them to add their own custom option too. + let dropdown_id = Ref::new(11); + let mut field = pdf.form_field(dropdown_id); + + // Choice fields come in two types, list and combo boxes. A combo box is + // also known as a dropdown menu, a list box is like a permanently expanded + // drop down menu. The edit flag allows the user to insert their own custom + // option. + // NOTE: at the time of writing this pdf.js (Firefox) does not allow + // editing of the box + field + .partial_name(TextStr("choice")) + .field_type(FieldType::Choice) + .field_flags(FieldFlags::COMBO | FieldFlags::EDIT); + + // Here we define the options the user will be presented with. + field.choice_options().options([ + TextStr("male"), + TextStr("female"), + TextStr("non-binary"), + TextStr("prefer not to say"), + ]); + + let mut annot = field.into_annotation(); + annot + .rect(Rect::new(108.0, 690.0, 208.0, 708.0)) + .flags(AnnotationFlags::PRINT); + annot.finish(); + + // PDFs can also have push buttons, buttons which retain no state when + // pressed. We'll use that to demonstrate form actions. Actions can be + // activated on many events, like a change in the input of a field, or + // simply the mous cursor moving over the annotation. + let button_id = Ref::new(12); + let mut field = pdf.form_field(button_id); + + // We set the push button field, otherwise it's interpreted to be a check + // box. + field + .partial_name(TextStr("button")) + .field_type(FieldType::Button) + .field_flags(FieldFlags::PUSHBUTTON); + + let mut annot = field.into_annotation(); + annot + .rect(Rect::new(108.0, 670.0, 138.0, 688.0)) + .flags(AnnotationFlags::PRINT); + + // We can quickly give it some basic appearance characteristics like + // background and border color. + annot.appearance_characteristics().border_color_gray(0.5); + + // Finally, we set the action that is taken when the button is pushed. + // It should reset fields in the form, but we must tell it which fields. + // By setting the `FormActionFlags::INCLUDE_EXCLUDE` flag, we tell it to + // exclude all fields in the we specify and by specifying no fields we + // ensure all fields are reset. + annot + .action() + .form_flags(FormActionFlags::INCLUDE_EXCLUDE) + .action_type(ActionType::ResetForm) + .fields(); + annot.finish(); + + // The PDF catalog contains the form dictionary, telling the reader that + // this document contains interactive form fields. + let catalog_id = Ref::new(13); + let page_tree_id = Ref::new(14); + let mut cat = pdf.catalog(catalog_id); + cat.pages(page_tree_id); + + // We write all root fields in to the form field dictionary. Root fields are + // those which have no parent. + cat.form() + .fields([text_field_id, radio_group_id, dropdown_id, button_id]); + cat.finish(); + + // First we create a page which should contain the form fields and write + // its resources. + let page_id = Ref::new(15); + let mut page = pdf.page(page_id); + page.media_box(Rect::new(0.0, 0.0, 595.0, 842.0)) + .parent(page_tree_id) + .resources() + .fonts() + .pair(text_font_name, text_font_id); + + // Now we write each widget annotations refereence into the annotations + // array. Those are our terminal fields, those with no children. + page.annotations([ + text_field_id, + radios[0].0, + radios[1].0, + radios[2].0, + dropdown_id, + button_id, + ]); + page.finish(); + + // Finally we write the font and page tree. + pdf.type1_font(text_font_id).base_font(Name(b"Helvetica")); + pdf.type1_font(symbol_font_id).base_font(Name(b"ZapfDingbats")); + pdf.pages(page_tree_id).kids([page_id]).count(1); + + std::fs::write("target/forms.pdf", pdf.finish()) +} diff --git a/src/actions.rs b/src/actions.rs index 14ca559..d38bfe4 100644 --- a/src/actions.rs +++ b/src/actions.rs @@ -154,11 +154,26 @@ impl<'a> Fields<'a> { self } + /// The indirect references to the fields. + pub fn ids(&mut self, ids: impl IntoIterator) -> &mut Self { + self.array.items(ids); + self + } + /// The fully qualified name of the field. PDF 1.3+. pub fn name(&mut self, name: TextStr) -> &mut Self { self.array.item(name); self } + + /// The fully qualified names of the fields. PDF 1.3+. + pub fn names<'b>( + &mut self, + names: impl IntoIterator>, + ) -> &mut Self { + self.array.items(names); + self + } } deref!('a, Fields<'a> => Array<'a>, array); @@ -219,46 +234,46 @@ bitflags::bitflags! { const INCLUDE_NO_VALUE_FIELDS = 2; /// Export the fields as HTML instead of submitting as FDF. Ignored if /// `SUBMIT_PDF` or `XFDF` are set. - const EXPORT_FORMAT = 1 << 3; + const EXPORT_FORMAT = 1 << 2; /// Field name should be submitted using an HTTP GET request, otherwise /// POST. Should only be if `EXPORT_FORMAT` is also set. - const GET_METHOD = 1 << 4; + const GET_METHOD = 1 << 3; /// Include the coordinates of the mouse when submit was pressed. Should /// only be if `EXPORT_FORMAT` is also set. - const SUBMIT_COORDINATES = 1 << 5; + const SUBMIT_COORDINATES = 1 << 4; /// Submit field names and values as XFDF instead of submitting an FDF. /// Should not be set if `SUBMIT_PDF` is set. PDF1.4+. - const XFDF = 1 << 6; + const XFDF = 1 << 5; /// Include all updates done to the PDF document in the submission FDF /// file. Should only be used when `XFDF` and `EXPORT_FORMAT` are not /// set. PDF 1.4+. - const INCLUDE_APPEND_SAVES = 1 << 7; + const INCLUDE_APPEND_SAVES = 1 << 6; /// Include all markup annotations of the PDF dcoument in the submission /// FDF file. Should only be used when `XFDF` and `EXPORT_FORMAT` are /// not set. PDF 1.4+. - const INCLUDE_ANNOTATIONS = 1 << 8; + const INCLUDE_ANNOTATIONS = 1 << 7; /// Submit the PDF file instead of an FDF file. All other flags other /// than `GET_METHOD` are ignored if this is set. PDF 1.4+. - const SUBMIT_PDF = 1 << 9; + const SUBMIT_PDF = 1 << 8; /// Convert fields which represent dates into the /// [canonical date format](crate::types::Date). The interpretation of /// a form field as a date is is not specified in the field but the /// JavaScript code that processes it. PDF 1.4+. - const CANONICAL_FORMAT = 1 << 10; + const CANONICAL_FORMAT = 1 << 9; /// Include only the markup annotations made by the current user (the /// `/T` entry of the annotation) as determined by the remote server /// the form will be submitted to. Should only be used when `XFDF` and /// `EXPORT_FORMAT` are not set and `INCLUDE_ANNOTATIONS` is set. PDF /// 1.4+. - const EXCLUDE_NON_USER_ANNOTS = 1 << 11; + const EXCLUDE_NON_USER_ANNOTS = 1 << 10; /// Include the F entry in the FDF file. /// Should only be used when `XFDF` and `EXPORT_FORMAT` are not set. /// PDF 1.4+ - const EXCLUDE_F_KEY = 1 << 12; + const EXCLUDE_F_KEY = 1 << 11; /// Include the PDF file as a stream in the FDF file that will be submitted. /// Should only be used when `XFDF` and `EXPORT_FORMAT` are not set. /// PDF 1.5+. - const EMBED_FORM = 1 << 14; + const EMBED_FORM = 1 << 13; } } diff --git a/src/annotations.rs b/src/annotations.rs index 98e6dd3..f1b2b4f 100644 --- a/src/annotations.rs +++ b/src/annotations.rs @@ -4,7 +4,7 @@ use super::*; /// /// An array of this struct is created by [`Chunk::annotation`]. pub struct Annotation<'a> { - dict: Dict<'a>, + pub(crate) dict: Dict<'a>, } writer!(Annotation: |obj| { @@ -211,7 +211,7 @@ impl<'a> Annotation<'a> { /// Start writing the `/MK` dictionary. Only permissible for the subtype /// `Widget`. pub fn appearance_characteristics(&mut self) -> AppearanceCharacteristics<'_> { - self.dict.insert(Name(b"MK")).start() + self.insert(Name(b"MK")).start() } /// Write the `/Parent` attribute. Only permissible for the subtype diff --git a/src/forms.rs b/src/forms.rs index 3d99582..1bdcdb8 100644 --- a/src/forms.rs +++ b/src/forms.rs @@ -1,6 +1,85 @@ +use crate::types::AnnotationType; + use super::*; -/// A form field. +/// Writer for an _interactive forms dictionary_. PDF 1.2+. +/// +/// This struct is created by [`Catalog::form`]. +pub struct Form<'a> { + dict: Dict<'a>, +} + +writer!(Form: |obj| Self { dict: obj.dict() }); + +impl<'a> Form<'a> { + /// Write the `/Fields` attribute to reference the root [form fields](Field) + /// (those who have no immediate parent) of this document. + pub fn fields(&mut self, fields: impl IntoIterator) -> &mut Self { + self.insert(Name(b"Fields")).array().items(fields); + self + } + + /// Write the `/SigFlags` attribute to set various document-level + /// characteristics related to signature fields. + pub fn sig_flags(&mut self, flags: SigFlags) -> &mut Self { + self.pair(Name(b"SigFlags"), flags.bits() as i32); + self + } + + /// Write the `/CO` attribute to set the field dictionaries with calculation + /// actions, defining the calculation order in which their values will be + /// recalculated when the value of any field changes. + pub fn calculation_order( + &mut self, + actions: impl IntoIterator, + ) -> &mut Self { + self.insert(Name(b"CO")).array().items(actions); + self + } + + /// Start writing the `/DR` attribute to set the default resources + /// that shall be used by form field appearance streams. At a minimum, this + /// dictionary shall contain a font entry specifying the resource name and + /// font dictionary of the default font for displaying text. + pub fn default_resources(&mut self) -> Resources<'_> { + self.insert(Name(b"DR")).start() + } + + /// Write the document-wide default value for the `/DA` attribute of + /// fields containing variable text. See + /// [`Field::vartext_default_appearance`]. + pub fn default_appearance(&mut self, default: Str) -> &mut Self { + self.pair(Name(b"DA"), default); + self + } + + /// Write the document-wide default value for the `/Q` attribute of + /// fields containing variable text. See [`Field::vartext_quadding`]. + pub fn quadding(&mut self, default: Quadding) -> &mut Self { + self.pair(Name(b"Q"), default as i32); + self + } +} + +deref!('a, Form<'a> => Dict<'a>, dict); + +bitflags::bitflags! { + /// Bitflags describing various document-level characteristics related to + /// signature fields. + pub struct SigFlags: u32 { + /// The document contains at least one signature field. + const SIGNATURES_EXIST = 1; + + /// The document contains signatures that may be invalidated if the + /// file is saved (written) in a way that alters its previous contents, + /// as opposed to an incremental update. Merely updating the file by + /// appending new information to the end of the previous version is + /// safe. + const APPEND_ONLY = 2; + } +} + +/// Writer for an _form field dictionary_. /// /// This struct is created by [`Chunk::form_field`]. pub struct Field<'a> { @@ -13,22 +92,23 @@ writer!(Field: |obj| Self { dict: obj.dict() }); 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 { - self.dict.pair(Name(b"FT"), typ.to_name()); + self.pair(Name(b"FT"), typ.to_name()); self } /// Write the `/Parent` attribute to set the immediate parent of this /// field. pub fn parent(&mut self, id: Ref) -> &mut Self { - self.dict.pair(Name(b"Parent"), id); + self.pair(Name(b"Parent"), id); self } - /// Start writing the `/Kids` attribute to set the immediate children of - /// this field. These references shall refer to other [fields][Field], or - /// [widget](crate::types::AnnotationType::Widget) [annoations](Annotation). - pub fn children(&mut self) -> TypedArray<'_, Ref> { - self.dict.insert(Name(b"Kids")).array().typed() + /// Write the `/Kids` attribute to set the immediate children of this field. + /// These references shall refer to other [fields][Field], or + /// [widget](crate::types::AnnotationType::Widget) [annotations](Annotation). + pub fn children(&mut self, children: impl IntoIterator) -> &mut Self { + self.insert(Name(b"Kids")).array().items(children); + self } /// Write the `/T` attribute to set the partial field name. @@ -42,7 +122,7 @@ impl<'a> Field<'a> { /// in properties that specify their visual appearance. In particular, they /// should have the same `/FT`, `/V` and `/DV` attribute values. pub fn partial_name(&mut self, name: TextStr) -> &mut Self { - self.dict.pair(Name(b"T"), name); + self.pair(Name(b"T"), name); self } @@ -53,7 +133,7 @@ impl<'a> Field<'a> { /// contents in support of accessibility to users with disabilities or for /// other purposes. PDF 1.3+. pub fn alternate_name(&mut self, alternate: TextStr) -> &mut Self { - self.dict.pair(Name(b"TU"), alternate); + self.pair(Name(b"TU"), alternate); self } @@ -61,21 +141,35 @@ impl<'a> Field<'a> { /// name shall be used when exporting interactive form field data from the /// document. pub fn mapping_name(&mut self, name: TextStr) -> &mut Self { - self.dict.pair(Name(b"TM"), name); + self.pair(Name(b"TM"), name); self } /// Write the `/Ff` attribute to set various characteristics of this /// field. pub fn field_flags(&mut self, flags: FieldFlags) -> &mut Self { - self.dict.pair(Name(b"Tf"), flags.bits() as i32); + self.pair(Name(b"Ff"), flags.bits() as i32); self } /// Start writing the `/AA` dictionary to set the field's response to /// various trigger events. pub fn additional_actions(&mut self) -> AdditionalActions<'_> { - self.dict.insert(Name(b"AA")).start() + self.insert(Name(b"AA")).start() + } + + /// Finish writing this field as a widget annotation. This is encouraged + /// for fields which are non-root and terminal (i.e. they have a parent and + /// no children). + /// + /// While the widget annotation could be a single child to a + /// terminal field, most readers will not correctly read the form + /// field, if it's not merged with its annotation. + pub fn into_annotation(mut self) -> Annotation<'a> { + self.dict.pair(Name(b"Type"), Name(b"Annot")); + let mut annot = Annotation { dict: self.dict }; + annot.subtype(AnnotationType::Widget); + annot } } @@ -109,11 +203,15 @@ impl FieldType { /// Only permissible on button fields. 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. + /// Write 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() + pub fn button_options<'b>( + &mut self, + options: impl IntoIterator>, + ) -> &mut Self { + self.insert(Name(b"Opt")).array().items(options); + self } } @@ -124,7 +222,7 @@ impl<'a> Field<'a> { /// dictionary](AppearanceCharacteristics) 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.pair(Name(b"V"), state.to_name()); self } @@ -133,7 +231,7 @@ impl<'a> Field<'a> { /// dictionary](AppearanceCharacteristics) 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.pair(Name(b"DV"), state.to_name()); self } } @@ -157,67 +255,50 @@ impl CheckBoxState { /// 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](AppearanceCharacteristics) 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()); + /// Write the `/V` attribute to set the state of this radio button field. + /// The state corresponds to an appearance stream in the + /// [appearance subdictionary](Appearance) of this field's widget + /// [annotation](Annotation) and is either a custom name unique for + /// all unique fields, or `/Off`. Only permissible on radio button fields. + pub fn radio_value(&mut self, state: Name) -> &mut Self { + self.pair(Name(b"V"), state); self } - /// Write the `/DV` attribute to set the default state of this check box + /// Write the `/DV` attribute to set the default state of this radio button /// field. The state corresponds to an appearance stream in the - /// [appearance dictionary](AppearanceCharacteristics) 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()); + /// [appearance subdictionary](Appearance) of this field's widget + /// [annotation](Annotation) and is either a custom name unique for + /// all unique fields, or `/Off`. Only permissible on radio button fields. + pub fn radio_default_value(&mut self, state: Name) -> &mut Self { + self.pair(Name(b"DV"), state); self } } -/// 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"), - } - } -} - /// 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 + /// Write the `/MaxLen` attribute to set the maximum length of the fields /// text in characters. Only permissible on text fields. + /// + /// The definition of a chracter depends on the encoding of the content of + /// `/V`. Which is either one byte for PDFDocEncoding or 2 for UTF16-BE. pub fn text_max_len(&mut self, len: i32) -> &mut Self { - self.dict.pair(Name(b"MaxLen"), len); + self.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.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.pair(Name(b"DV"), value); self } } @@ -229,7 +310,7 @@ impl<'a> Field<'a> { /// 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.pair(Name(b"DA"), appearance); self } @@ -237,21 +318,21 @@ impl<'a> Field<'a> { /// 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 i32); + self.pair(Name(b"Q"), quadding as i32); self } /// 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.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.pair(Name(b"RV"), value); self } } @@ -272,20 +353,20 @@ 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() + self.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.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`] + /// Write 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 @@ -294,8 +375,12 @@ impl<'a> Field<'a> { /// 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() + pub fn choice_indices( + &mut self, + indices: impl IntoIterator, + ) -> &mut Self { + self.insert(Name(b"I")).array().items(indices); + self } /// Write the `/V` attribute to set the currently selected values @@ -304,8 +389,8 @@ impl<'a> Field<'a> { /// permissible on choice fields. pub fn choice_value(&mut self, option: Option) -> &mut Self { match option { - Some(value) => self.dict.pair(Name(b"V"), value), - None => self.dict.pair(Name(b"V"), Null), + Some(value) => self.pair(Name(b"V"), value), + None => self.pair(Name(b"V"), Null), }; self } @@ -317,7 +402,7 @@ impl<'a> Field<'a> { &mut self, options: impl IntoIterator>, ) -> &mut Self { - self.dict.insert(Name(b"V")).array().items(options); + self.insert(Name(b"V")).array().items(options); self } @@ -327,8 +412,8 @@ impl<'a> Field<'a> { /// permissible on choice fields. pub fn choice_default_value(&mut self, option: Option) -> &mut Self { match option { - Some(value) => self.dict.pair(Name(b"DV"), value), - None => self.dict.pair(Name(b"DV"), Null), + Some(value) => self.pair(Name(b"DV"), value), + None => self.pair(Name(b"DV"), Null), }; self } @@ -340,7 +425,7 @@ impl<'a> Field<'a> { &mut self, options: impl IntoIterator>, ) -> &mut Self { - self.dict.insert(Name(b"DV")).array().items(options); + self.insert(Name(b"DV")).array().items(options); self } } @@ -361,11 +446,31 @@ impl<'a> ChoiceOptions<'a> { self } + /// Add options with the given values. + pub fn options<'b>( + &mut self, + values: impl IntoIterator>, + ) -> &mut Self { + self.array.items(values); + 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 } + + /// Add options with the given pairs of value and export value. + pub fn exports<'b>( + &mut self, + values: impl IntoIterator, TextStr<'b>)>, + ) -> &mut Self { + for (value, export) in values { + self.export(value, export); + } + self + } } deref!('a, ChoiceOptions<'a> => Array<'a>, array); @@ -384,10 +489,10 @@ bitflags::bitflags! { const REQUIRED = 2; /// The field shall not be exported by a /// [submit-form](crate::types::ActionType::SubmitForm)[`Action`]. - const NO_EXPORT = 1 << 3; + const NO_EXPORT = 1 << 2; /// The entered text shall not be spell-checked, can be used for text /// and choice fields. - const DO_NOT_SPELL_CHECK = 1 << 23; + const DO_NOT_SPELL_CHECK = 1 << 22; // Button specific flags @@ -395,62 +500,63 @@ bitflags::bitflags! { /// 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; + const NO_TOGGLE_TO_OFF = 1 << 14; /// 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; + const RADIO = 1 << 15; /// The field is a push button that does not retain a permanent /// value. - const PUSHBUTTON = 1 << 17; + const PUSHBUTTON = 1 << 16; /// 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; + const RADIOS_IN_UNISON = 1 << 25; // Text field specific flags /// The text may contain multiple lines of text, otherwise the text is /// restricted to one line. - const MULTILINE = 1 << 13; + const MULTILINE = 1 << 12; /// The text contains a password and should not be echoed visibly to /// the screen. - const PASSWORD = 1 << 14; + const PASSWORD = 1 << 13; /// 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; + const FILE_SELECT = 1 << 20; /// 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`] + const DO_NOT_SCROLL = 1 << 23; + /// The field shall be automatically divided into as many equally + /// spaced positions 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; + const COMB = 1 << 24; /// 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; + const RICH_TEXT = 1 << 25; // Choice field specific flags - /// The field is a combo box if set, else it's a list box. - const COMBO = 1 << 18; + /// The field is a combo box if set, else it's a list box. A combo box + /// is often referred to as a dropdown menu. + const COMBO = 1 << 17; /// 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; + const EDIT = 1 << 18; /// The field’s option items shall be sorted alphabetically. This /// flag is intended for use by writers, not by readers. - const SORT = 1 << 20; + const SORT = 1 << 19; /// More than one option of the choice field may be selected /// simultaneously. PDF 1.4+. - const MULTI_SELECT = 1 << 22; + const MULTI_SELECT = 1 << 21; /// 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, @@ -460,6 +566,6 @@ bitflags::bitflags! { /// /// 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; + const COMMIT_ON_SEL_CHANGE = 1 << 26; } } diff --git a/src/lib.rs b/src/lib.rs index 93f9c00..f2f43ef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -130,7 +130,7 @@ pub mod writers { CidFont, Cmap, Differences, Encoding, FontDescriptor, Type0Font, Type1Font, Type3Font, Widths, }; - pub use forms::Field; + pub use forms::{Field, Form}; pub use functions::{ ExponentialFunction, PostScriptFunction, SampledFunction, StitchingFunction, }; @@ -168,7 +168,7 @@ pub mod types { }; pub use font::{CidFontType, FontFlags, FontStretch, SystemInfo, UnicodeCmap}; pub use forms::{ - CheckBoxState, ChoiceOptions, FieldFlags, FieldType, Quadding, RadioState, + CheckBoxState, ChoiceOptions, FieldFlags, FieldType, Quadding, SigFlags, }; pub use functions::{InterpolationOrder, PostScriptOp}; pub use renditions::{MediaClipType, RenditionType, TempFileType}; diff --git a/src/structure.rs b/src/structure.rs index 57e2fad..a5def42 100644 --- a/src/structure.rs +++ b/src/structure.rs @@ -97,6 +97,12 @@ impl<'a> Catalog<'a> { self.insert(Name(b"AA")).start() } + /// Start writing the `/AcroForm` dictionary to specify the document wide + /// form. PDF 1.2+. + pub fn form(&mut self) -> Form<'_> { + self.insert(Name(b"AcroForm")).start() + } + /// Write the `/Metadata` attribute to specify the document's metadata. PDF /// 1.4+. ///