|
| 1 | +//! This example shows how to create forms accepted by the most popular readers. |
| 2 | +
|
| 3 | +use pdf_writer::types::{ |
| 4 | + ActionType, AnnotationFlags, BorderType, FieldFlags, FieldType, FormActionFlags, |
| 5 | +}; |
| 6 | +use pdf_writer::{Content, Finish, Name, Pdf, Rect, Ref, Str, TextStr}; |
| 7 | + |
| 8 | +fn main() -> std::io::Result<()> { |
| 9 | + let mut pdf = Pdf::new(); |
| 10 | + |
| 11 | + // Let's set up our primary font, we'll have to reference it a few times. |
| 12 | + let text_font_id = Ref::new(1); |
| 13 | + let text_font_name = Name(b"F1"); |
| 14 | + |
| 15 | + // Here we'll set up our Dingbat font, this is used for symbols such as the |
| 16 | + // ticks in checkboxes. |
| 17 | + let symbol_font_id = Ref::new(2); |
| 18 | + let symbol_font_name = Name(b"F2"); |
| 19 | + |
| 20 | + // One of the most common form field types is the text field. Let's add that |
| 21 | + // and look at some of the basics of PDF form fields. |
| 22 | + let text_field_id = Ref::new(4); |
| 23 | + |
| 24 | + // We start by writing a form field dictionary with an id which we later |
| 25 | + // need for referencing it. |
| 26 | + let mut field = pdf.form_field(text_field_id); |
| 27 | + |
| 28 | + // While the `/T` attribute is optional according to the spec, you should |
| 29 | + // include it, most readers will only render widget annotations with both |
| 30 | + // partial name and field type. Next, we set it's value and default value: |
| 31 | + // - The value is used to store what the user has put into the field. |
| 32 | + // - The default value is used when resetting the form. |
| 33 | + field |
| 34 | + .partial_name(TextStr("text")) |
| 35 | + .field_type(FieldType::Text) |
| 36 | + .text_value(TextStr("Hello")) |
| 37 | + .text_default_value(TextStr("Who reset me")); |
| 38 | + |
| 39 | + // Our field is a terminal field because it has no children, so it's merged |
| 40 | + // with its widget annotation. The widget annotation is what declares the |
| 41 | + // appearance and position in the document, whereas the field defines its |
| 42 | + // semantic behavior for the document-wide form. The appearance is more |
| 43 | + // relevant to button fields, we'll see how to cofigure it below. |
| 44 | + let mut annot = field.into_annotation(); |
| 45 | + annot.rect(Rect::new(108.0, 730.0, 208.0, 748.0)); |
| 46 | + |
| 47 | + // We can pass some fairly simple appearances here, common things such |
| 48 | + // as the border color and style. This will give out field a purple |
| 49 | + // underline, keep in mind that this may be drowned out by the viewer's |
| 50 | + // form highlighting. |
| 51 | + annot.border_style().style(BorderType::Underline); |
| 52 | + annot.appearance_characteristics().border_color_rgb(0.0, 0.0, 0.5); |
| 53 | + |
| 54 | + // The reader will usually provide a default appearance and automatically |
| 55 | + // highlight form fields. The appearance is relevant for printing however. |
| 56 | + // While we don't provide an explicit appearnce here, if we did we likely |
| 57 | + // want this flag to be set. |
| 58 | + annot.flags(AnnotationFlags::PRINT); |
| 59 | + annot.finish(); |
| 60 | + |
| 61 | + // A good form has radio buttons. Radio buttons are checkboxes which turn |
| 62 | + // off when another checkbox is turned on. A group of radio button widget |
| 63 | + // annotations shares a single radio button field as parent. |
| 64 | + let radio_group_id = Ref::new(5); |
| 65 | + |
| 66 | + // The FormXObjects for our checkboxes need bounding boxes, in this case |
| 67 | + // these are the same size as out rectangles, but within their coordinate |
| 68 | + // system. |
| 69 | + let bbox = Rect::new(0.0, 0.0, 30.0, 18.0); |
| 70 | + |
| 71 | + // We define our three radio buttons, they all have a different appearance |
| 72 | + // streams, but if they shared the same appearance stream and used the |
| 73 | + // RADIOS_IN_UNISON flag, then two buttons could refer to the same choice. |
| 74 | + // This is not widely supported, so we'll simply showcase some normal radio |
| 75 | + // buttons here. |
| 76 | + // |
| 77 | + // NOTE: A reader like Okular will also use on-state name in the default |
| 78 | + // appearance. |
| 79 | + let radios = [ |
| 80 | + (Ref::new(6), Rect::new(108.0, 710.0, 138.0, 728.0), b"ch1"), |
| 81 | + (Ref::new(7), Rect::new(140.0, 710.0, 170.0, 728.0), b"ch2"), |
| 82 | + (Ref::new(8), Rect::new(172.0, 710.0, 202.0, 728.0), b"ch3"), |
| 83 | + ]; |
| 84 | + // First, we define the radio group parent. The children of this field will |
| 85 | + // be our actual buttons. We can define most of the radio related properties |
| 86 | + // here. |
| 87 | + let mut field = pdf.form_field(radio_group_id); |
| 88 | + |
| 89 | + // We set some flags to get the exact behavior we want. |
| 90 | + // - FieldFlags::NO_TOGGLE_OFF means that once a button is selected it |
| 91 | + // cannot be manually turned off without turning another button on. |
| 92 | + // - FieldFlags::RADIOS_IN_UNISON ensures that if we have buttons which use |
| 93 | + // the same appearance on-state, they'll be toggled in unison with the |
| 94 | + // others (although we don't use this here). |
| 95 | + // Finally we define the children of this field, the widget annotations |
| 96 | + // which again define appearance and postion of the individual buttons. |
| 97 | + // |
| 98 | + // NOTE: by the time of writing this, RADIOS_IN_UNISON does not work |
| 99 | + // correctly pdf.js (firefox), okular or evince. |
| 100 | + field |
| 101 | + .partial_name(TextStr("radio")) |
| 102 | + .field_type(FieldType::Button) |
| 103 | + .field_flags( |
| 104 | + FieldFlags::RADIO |
| 105 | + | FieldFlags::NO_TOGGLE_TO_OFF |
| 106 | + | FieldFlags::RADIOS_IN_UNISON, |
| 107 | + ) |
| 108 | + .children(radios.map(|(id, _, _)| id)); |
| 109 | + field.finish(); |
| 110 | + |
| 111 | + // For buttons appearances are more relevant when printing as they're |
| 112 | + // usually not as easy to find as text fields if they have no appearance. |
| 113 | + let radio_on_appearance_id = Ref::new(9); |
| 114 | + let radio_off_appearance_id = Ref::new(10); |
| 115 | + |
| 116 | + // Here we prepare our appearances, the on appearance is a tick and the off |
| 117 | + // appearance is empty. |
| 118 | + let mut content = Content::new(); |
| 119 | + content.save_state(); |
| 120 | + content.begin_text(); |
| 121 | + content.set_fill_gray(0.0); |
| 122 | + content.set_font(symbol_font_name, 14.0); |
| 123 | + // The character 4 is a tick in this font. |
| 124 | + content.show(Str(b"4")); |
| 125 | + content.end_text(); |
| 126 | + content.restore_state(); |
| 127 | + |
| 128 | + let on_stream = content.finish(); |
| 129 | + let mut on_appearance = pdf.form_xobject(radio_on_appearance_id, &on_stream); |
| 130 | + |
| 131 | + on_appearance.bbox(bbox); |
| 132 | + |
| 133 | + // We use the symbol font to display the tick, so we need to add it to the |
| 134 | + // resources of the appearance stream. |
| 135 | + on_appearance |
| 136 | + .resources() |
| 137 | + .fonts() |
| 138 | + .pair(symbol_font_name, symbol_font_id); |
| 139 | + |
| 140 | + on_appearance.finish(); |
| 141 | + |
| 142 | + // Our off appearance is empty, we haven't ticked the box. |
| 143 | + pdf.form_xobject(radio_off_appearance_id, &Content::new().finish()) |
| 144 | + .bbox(bbox); |
| 145 | + |
| 146 | + // Now we'll write a widget annotation for each button. |
| 147 | + for (id, rect, state) in radios { |
| 148 | + // While we create a field here we could directly create widget |
| 149 | + // annotation too. |
| 150 | + let mut field = pdf.form_field(id); |
| 151 | + |
| 152 | + // Each button shares the single parent. |
| 153 | + field.parent(radio_group_id); |
| 154 | + |
| 155 | + let mut annot = field.into_annotation(); |
| 156 | + annot.rect(rect).flags(AnnotationFlags::PRINT); |
| 157 | + |
| 158 | + // This is the state the button starts off with. `/Off` is the off state |
| 159 | + // and is the same for all radio buttons. The `on` state gets its own |
| 160 | + // name to distinguish different buttons. |
| 161 | + annot.appearance_state(Name(b"Off")); |
| 162 | + |
| 163 | + // Finally we set the appearance dictionary to contain a normal |
| 164 | + // appearance sub dictionary mapping both on and off state to the |
| 165 | + // respective FormXObject. |
| 166 | + { |
| 167 | + let mut appearance = annot.appearance(); |
| 168 | + appearance.normal().streams().pairs([ |
| 169 | + (Name(state), radio_on_appearance_id), |
| 170 | + (Name(b"Off"), radio_off_appearance_id), |
| 171 | + ]); |
| 172 | + } |
| 173 | + } |
| 174 | + |
| 175 | + // Let's add a dropdown menu and allow the user to chose from preconfigrued |
| 176 | + // options while allowing them to add their own custom option too. |
| 177 | + let dropdown_id = Ref::new(11); |
| 178 | + let mut field = pdf.form_field(dropdown_id); |
| 179 | + |
| 180 | + // Choice fields come in two types, list and combo boxes. A combo box is |
| 181 | + // also known as a dropdown menu, a list box is like a permanently expanded |
| 182 | + // drop down menu. The edit flag allows the user to insert their own custom |
| 183 | + // option. |
| 184 | + // NOTE: at the time of writing this pdf.js (Firefox) does not allow |
| 185 | + // editing of the box |
| 186 | + field |
| 187 | + .partial_name(TextStr("choice")) |
| 188 | + .field_type(FieldType::Choice) |
| 189 | + .field_flags(FieldFlags::COMBO | FieldFlags::EDIT); |
| 190 | + |
| 191 | + // Here we define the options the user will be presented with. |
| 192 | + field.choice_options().options([ |
| 193 | + TextStr("male"), |
| 194 | + TextStr("female"), |
| 195 | + TextStr("non-binary"), |
| 196 | + TextStr("prefer not to say"), |
| 197 | + ]); |
| 198 | + |
| 199 | + let mut annot = field.into_annotation(); |
| 200 | + annot |
| 201 | + .rect(Rect::new(108.0, 690.0, 208.0, 708.0)) |
| 202 | + .flags(AnnotationFlags::PRINT); |
| 203 | + annot.finish(); |
| 204 | + |
| 205 | + // PDFs can also have push buttons, buttons which retain no state when |
| 206 | + // pressed. We'll use that to demonstrate form actions. Actions can be |
| 207 | + // activated on many events, like a change in the input of a field, or |
| 208 | + // simply the mous cursor moving over the annotation. |
| 209 | + let button_id = Ref::new(12); |
| 210 | + let mut field = pdf.form_field(button_id); |
| 211 | + |
| 212 | + // We set the push button field, otherwise it's interpreted to be a check |
| 213 | + // box. |
| 214 | + field |
| 215 | + .partial_name(TextStr("button")) |
| 216 | + .field_type(FieldType::Button) |
| 217 | + .field_flags(FieldFlags::PUSHBUTTON); |
| 218 | + |
| 219 | + let mut annot = field.into_annotation(); |
| 220 | + annot |
| 221 | + .rect(Rect::new(108.0, 670.0, 138.0, 688.0)) |
| 222 | + .flags(AnnotationFlags::PRINT); |
| 223 | + |
| 224 | + // We can quickly give it some basic appearance characteristics like |
| 225 | + // background and border color. |
| 226 | + annot.appearance_characteristics().border_color_gray(0.5); |
| 227 | + |
| 228 | + // Finally, we set the action that is taken when the button is pushed. |
| 229 | + // It should reset fields in the form, but we must tell it which fields. |
| 230 | + // By setting the `FormActionFlags::INCLUDE_EXCLUDE` flag, we tell it to |
| 231 | + // exclude all fields in the we specify and by specifying no fields we |
| 232 | + // ensure all fields are reset. |
| 233 | + annot |
| 234 | + .action() |
| 235 | + .form_flags(FormActionFlags::INCLUDE_EXCLUDE) |
| 236 | + .action_type(ActionType::ResetForm) |
| 237 | + .fields(); |
| 238 | + annot.finish(); |
| 239 | + |
| 240 | + // The PDF catalog contains the form dictionary, telling the reader that |
| 241 | + // this document contains interactive form fields. |
| 242 | + let catalog_id = Ref::new(13); |
| 243 | + let page_tree_id = Ref::new(14); |
| 244 | + let mut cat = pdf.catalog(catalog_id); |
| 245 | + cat.pages(page_tree_id); |
| 246 | + |
| 247 | + // We write all root fields in to the form field dictionary. Root fields are |
| 248 | + // those which have no parent. |
| 249 | + cat.form() |
| 250 | + .fields([text_field_id, radio_group_id, dropdown_id, button_id]); |
| 251 | + cat.finish(); |
| 252 | + |
| 253 | + // First we create a page which should contain the form fields and write |
| 254 | + // its resources. |
| 255 | + let page_id = Ref::new(15); |
| 256 | + let mut page = pdf.page(page_id); |
| 257 | + page.media_box(Rect::new(0.0, 0.0, 595.0, 842.0)) |
| 258 | + .parent(page_tree_id) |
| 259 | + .resources() |
| 260 | + .fonts() |
| 261 | + .pair(text_font_name, text_font_id); |
| 262 | + |
| 263 | + // Now we write each widget annotations refereence into the annotations |
| 264 | + // array. Those are our terminal fields, those with no children. |
| 265 | + page.annotations([ |
| 266 | + text_field_id, |
| 267 | + radios[0].0, |
| 268 | + radios[1].0, |
| 269 | + radios[2].0, |
| 270 | + dropdown_id, |
| 271 | + button_id, |
| 272 | + ]); |
| 273 | + page.finish(); |
| 274 | + |
| 275 | + // Finally we write the font and page tree. |
| 276 | + pdf.type1_font(text_font_id).base_font(Name(b"Helvetica")); |
| 277 | + pdf.type1_font(symbol_font_id).base_font(Name(b"ZapfDingbats")); |
| 278 | + pdf.pages(page_tree_id).kids([page_id]).count(1); |
| 279 | + |
| 280 | + std::fs::write("target/forms.pdf", pdf.finish()) |
| 281 | +} |
0 commit comments