Skip to content

Commit

Permalink
Feature: Interactive Form Dictionary (PDF 1.7 Section 12.7.2) & Appea…
Browse files Browse the repository at this point in the history
…rance Streams (12.5.5) (#25)
  • Loading branch information
tingerrr authored Apr 30, 2024
1 parent d2c2550 commit f4a55a9
Show file tree
Hide file tree
Showing 6 changed files with 516 additions and 108 deletions.
281 changes: 281 additions & 0 deletions examples/forms.rs
Original file line number Diff line number Diff line change
@@ -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())
}
37 changes: 26 additions & 11 deletions src/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,11 +154,26 @@ impl<'a> Fields<'a> {
self
}

/// The indirect references to the fields.
pub fn ids(&mut self, ids: impl IntoIterator<Item = Ref>) -> &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<Item = TextStr<'b>>,
) -> &mut Self {
self.array.items(names);
self
}
}

deref!('a, Fields<'a> => Array<'a>, array);
Expand Down Expand Up @@ -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;
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/annotations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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| {
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit f4a55a9

Please sign in to comment.