Skip to content

Commit f4a55a9

Browse files
authored
Feature: Interactive Form Dictionary (PDF 1.7 Section 12.7.2) & Appearance Streams (12.5.5) (#25)
1 parent d2c2550 commit f4a55a9

6 files changed

Lines changed: 516 additions & 108 deletions

File tree

examples/forms.rs

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
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+
}

src/actions.rs

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -154,11 +154,26 @@ impl<'a> Fields<'a> {
154154
self
155155
}
156156

157+
/// The indirect references to the fields.
158+
pub fn ids(&mut self, ids: impl IntoIterator<Item = Ref>) -> &mut Self {
159+
self.array.items(ids);
160+
self
161+
}
162+
157163
/// The fully qualified name of the field. PDF 1.3+.
158164
pub fn name(&mut self, name: TextStr) -> &mut Self {
159165
self.array.item(name);
160166
self
161167
}
168+
169+
/// The fully qualified names of the fields. PDF 1.3+.
170+
pub fn names<'b>(
171+
&mut self,
172+
names: impl IntoIterator<Item = TextStr<'b>>,
173+
) -> &mut Self {
174+
self.array.items(names);
175+
self
176+
}
162177
}
163178

164179
deref!('a, Fields<'a> => Array<'a>, array);
@@ -219,46 +234,46 @@ bitflags::bitflags! {
219234
const INCLUDE_NO_VALUE_FIELDS = 2;
220235
/// Export the fields as HTML instead of submitting as FDF. Ignored if
221236
/// `SUBMIT_PDF` or `XFDF` are set.
222-
const EXPORT_FORMAT = 1 << 3;
237+
const EXPORT_FORMAT = 1 << 2;
223238
/// Field name should be submitted using an HTTP GET request, otherwise
224239
/// POST. Should only be if `EXPORT_FORMAT` is also set.
225-
const GET_METHOD = 1 << 4;
240+
const GET_METHOD = 1 << 3;
226241
/// Include the coordinates of the mouse when submit was pressed. Should
227242
/// only be if `EXPORT_FORMAT` is also set.
228-
const SUBMIT_COORDINATES = 1 << 5;
243+
const SUBMIT_COORDINATES = 1 << 4;
229244
/// Submit field names and values as XFDF instead of submitting an FDF.
230245
/// Should not be set if `SUBMIT_PDF` is set. PDF1.4+.
231-
const XFDF = 1 << 6;
246+
const XFDF = 1 << 5;
232247
/// Include all updates done to the PDF document in the submission FDF
233248
/// file. Should only be used when `XFDF` and `EXPORT_FORMAT` are not
234249
/// set. PDF 1.4+.
235-
const INCLUDE_APPEND_SAVES = 1 << 7;
250+
const INCLUDE_APPEND_SAVES = 1 << 6;
236251
/// Include all markup annotations of the PDF dcoument in the submission
237252
/// FDF file. Should only be used when `XFDF` and `EXPORT_FORMAT` are
238253
/// not set. PDF 1.4+.
239-
const INCLUDE_ANNOTATIONS = 1 << 8;
254+
const INCLUDE_ANNOTATIONS = 1 << 7;
240255
/// Submit the PDF file instead of an FDF file. All other flags other
241256
/// than `GET_METHOD` are ignored if this is set. PDF 1.4+.
242-
const SUBMIT_PDF = 1 << 9;
257+
const SUBMIT_PDF = 1 << 8;
243258
/// Convert fields which represent dates into the
244259
/// [canonical date format](crate::types::Date). The interpretation of
245260
/// a form field as a date is is not specified in the field but the
246261
/// JavaScript code that processes it. PDF 1.4+.
247-
const CANONICAL_FORMAT = 1 << 10;
262+
const CANONICAL_FORMAT = 1 << 9;
248263
/// Include only the markup annotations made by the current user (the
249264
/// `/T` entry of the annotation) as determined by the remote server
250265
/// the form will be submitted to. Should only be used when `XFDF` and
251266
/// `EXPORT_FORMAT` are not set and `INCLUDE_ANNOTATIONS` is set. PDF
252267
/// 1.4+.
253-
const EXCLUDE_NON_USER_ANNOTS = 1 << 11;
268+
const EXCLUDE_NON_USER_ANNOTS = 1 << 10;
254269
/// Include the F entry in the FDF file.
255270
/// Should only be used when `XFDF` and `EXPORT_FORMAT` are not set.
256271
/// PDF 1.4+
257-
const EXCLUDE_F_KEY = 1 << 12;
272+
const EXCLUDE_F_KEY = 1 << 11;
258273
/// Include the PDF file as a stream in the FDF file that will be submitted.
259274
/// Should only be used when `XFDF` and `EXPORT_FORMAT` are not set.
260275
/// PDF 1.5+.
261-
const EMBED_FORM = 1 << 14;
276+
const EMBED_FORM = 1 << 13;
262277
}
263278
}
264279

src/annotations.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use super::*;
44
///
55
/// An array of this struct is created by [`Chunk::annotation`].
66
pub struct Annotation<'a> {
7-
dict: Dict<'a>,
7+
pub(crate) dict: Dict<'a>,
88
}
99

1010
writer!(Annotation: |obj| {
@@ -211,7 +211,7 @@ impl<'a> Annotation<'a> {
211211
/// Start writing the `/MK` dictionary. Only permissible for the subtype
212212
/// `Widget`.
213213
pub fn appearance_characteristics(&mut self) -> AppearanceCharacteristics<'_> {
214-
self.dict.insert(Name(b"MK")).start()
214+
self.insert(Name(b"MK")).start()
215215
}
216216

217217
/// Write the `/Parent` attribute. Only permissible for the subtype

0 commit comments

Comments
 (0)