use crate::color::Color;
use crate::font::BuiltinFont;
use crate::object::{ObjRef, PdfDict, PdfObject};
use crate::writer::PdfWriter;
#[derive(Debug, Clone, Copy)]
pub enum FieldAlign {
Left = 0,
Center = 1,
Right = 2,
}
#[derive(Debug, Clone)]
pub struct FieldRect {
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
}
impl FieldRect {
pub fn new(x: f64, y: f64, width: f64, height: f64) -> Self {
Self { x, y, width, height }
}
fn to_pdf_array(&self) -> PdfObject {
PdfObject::Array(vec![
PdfObject::Real(self.x),
PdfObject::Real(self.y),
PdfObject::Real(self.x + self.width),
PdfObject::Real(self.y + self.height),
])
}
}
#[derive(Debug, Clone)]
pub enum FieldKind {
Text {
default_value: Option<String>,
multiline: bool,
password: bool,
max_len: Option<u32>,
},
Checkbox {
checked: bool,
export_value: String,
},
Radio {
group: String,
export_value: String,
selected: bool,
},
Dropdown {
options: Vec<String>,
selected: Option<String>,
editable: bool,
},
ListBox {
options: Vec<String>,
selected: Vec<String>,
multi_select: bool,
},
Button {
label: String,
},
}
#[derive(Debug, Clone)]
pub struct FormField {
pub name: String,
pub tooltip: Option<String>,
pub kind: FieldKind,
pub rect: FieldRect,
pub page: usize,
pub font: BuiltinFont,
pub font_size: f64,
pub text_color: Color,
pub bg_color: Option<Color>,
pub border_color: Option<Color>,
pub border_width: f64,
pub align: FieldAlign,
pub read_only: bool,
pub required: bool,
}
impl FormField {
pub fn text(name: impl Into<String>, rect: FieldRect, page: usize) -> Self {
Self::base(name, FieldKind::Text {
default_value: None,
multiline: false,
password: false,
max_len: None,
}, rect, page)
}
pub fn multiline_text(name: impl Into<String>, rect: FieldRect, page: usize) -> Self {
Self::base(name, FieldKind::Text {
default_value: None,
multiline: true,
password: false,
max_len: None,
}, rect, page)
}
pub fn password(name: impl Into<String>, rect: FieldRect, page: usize) -> Self {
Self::base(name, FieldKind::Text {
default_value: None,
multiline: false,
password: true,
max_len: None,
}, rect, page)
}
pub fn checkbox(name: impl Into<String>, rect: FieldRect, page: usize, checked: bool) -> Self {
Self::base(name, FieldKind::Checkbox {
checked,
export_value: "Yes".into(),
}, rect, page)
}
pub fn radio(name: impl Into<String>, group: impl Into<String>, export_value: impl Into<String>, rect: FieldRect, page: usize, selected: bool) -> Self {
Self::base(name, FieldKind::Radio {
group: group.into(),
export_value: export_value.into(),
selected,
}, rect, page)
}
pub fn dropdown(name: impl Into<String>, options: Vec<String>, rect: FieldRect, page: usize) -> Self {
Self::base(name, FieldKind::Dropdown {
options,
selected: None,
editable: false,
}, rect, page)
}
pub fn button(name: impl Into<String>, label: impl Into<String>, rect: FieldRect, page: usize) -> Self {
Self::base(name, FieldKind::Button { label: label.into() }, rect, page)
}
fn base(name: impl Into<String>, kind: FieldKind, rect: FieldRect, page: usize) -> Self {
Self {
name: name.into(),
tooltip: None,
kind,
rect,
page,
font: BuiltinFont::Helvetica,
font_size: 10.0,
text_color: Color::BLACK,
bg_color: Some(Color::WHITE),
border_color: Some(Color::rgb_u8(150, 150, 150)),
border_width: 1.0,
align: FieldAlign::Left,
read_only: false,
required: false,
}
}
pub fn value(mut self, v: impl Into<String>) -> Self {
if let FieldKind::Text { ref mut default_value, .. } = self.kind {
*default_value = Some(v.into());
}
self
}
pub fn tooltip(mut self, t: impl Into<String>) -> Self {
self.tooltip = Some(t.into());
self
}
pub fn font_size(mut self, s: f64) -> Self {
self.font_size = s;
self
}
pub fn bg(mut self, c: Color) -> Self {
self.bg_color = Some(c);
self
}
pub fn border(mut self, c: Color, w: f64) -> Self {
self.border_color = Some(c);
self.border_width = w;
self
}
pub fn read_only(mut self) -> Self {
self.read_only = true;
self
}
pub fn required(mut self) -> Self {
self.required = true;
self
}
pub fn align(mut self, a: FieldAlign) -> Self {
self.align = a;
self
}
pub fn max_len(mut self, n: u32) -> Self {
if let FieldKind::Text { ref mut max_len, .. } = self.kind {
*max_len = Some(n);
}
self
}
}
pub struct AcroForm {
pub fields: Vec<FormField>,
pub need_appearances: bool,
}
impl AcroForm {
pub fn new() -> Self {
Self { fields: Vec::new(), need_appearances: true }
}
pub fn add(mut self, field: FormField) -> Self {
self.fields.push(field);
self
}
pub fn is_empty(&self) -> bool {
self.fields.is_empty()
}
pub fn write(
&self,
writer: &mut PdfWriter,
page_refs: &[ObjRef],
) -> (PdfDict, Vec<(usize, ObjRef)>) {
let mut field_refs: Vec<ObjRef> = Vec::new();
let mut widget_placement: Vec<(usize, ObjRef)> = Vec::new();
for field in &self.fields {
let field_ref = self.write_field(writer, field, page_refs);
field_refs.push(field_ref);
widget_placement.push((field.page, field_ref));
}
let mut acroform = PdfDict::new();
acroform.set(
"Fields",
PdfObject::Array(field_refs.iter().map(|r| PdfObject::Reference(*r)).collect()),
);
acroform.set("NeedAppearances", PdfObject::Boolean(self.need_appearances));
let mut dr = PdfDict::new();
let mut font_dict = PdfDict::new();
let mut helv = PdfDict::new();
helv.set("Type", PdfObject::name("Font"));
helv.set("Subtype", PdfObject::name("Type1"));
helv.set("BaseFont", PdfObject::name("Helvetica"));
font_dict.set("Helv", PdfObject::Dictionary(helv));
dr.set("Font", PdfObject::Dictionary(font_dict));
acroform.set("DR", PdfObject::Dictionary(dr));
acroform.set("DA", PdfObject::string("/Helv 10 Tf 0 g"));
(acroform, widget_placement)
}
fn write_field(&self, writer: &mut PdfWriter, field: &FormField, page_refs: &[ObjRef]) -> ObjRef {
let field_ref = writer.reserve();
let mut dict = PdfDict::new();
dict.set("Type", PdfObject::name("Annot"));
dict.set("Subtype", PdfObject::name("Widget"));
dict.set("Rect", field.rect.to_pdf_array());
if let Some(&page_ref) = page_refs.get(field.page) {
dict.set("P", PdfObject::Reference(page_ref));
}
if let Some(ref tip) = field.tooltip {
dict.set("TU", PdfObject::string(tip.as_str()));
}
let mut ff: i64 = 0;
if field.read_only { ff |= 1; }
if field.required { ff |= 2; }
let da = format!("/Helv {} Tf {} g",
field.font_size as i32,
match field.text_color {
Color::Gray(g) => format!("{:.3}", g),
Color::Rgb(r, g, b) => format!("{:.3} {:.3} {:.3} rg", r, g, b),
_ => "0 g".into(),
}
);
match &field.kind {
FieldKind::Text { default_value, multiline, password, max_len } => {
dict.set("FT", PdfObject::name("Tx"));
dict.set("T", PdfObject::string(field.name.as_str()));
dict.set("DA", PdfObject::string(da.as_str()));
dict.set("Q", PdfObject::Integer(field.align as i64));
let mut field_flags: i64 = ff;
if *multiline { field_flags |= 1 << 12; }
if *password { field_flags |= 1 << 13; }
dict.set("Ff", PdfObject::Integer(field_flags));
if let Some(ref v) = default_value {
dict.set("V", PdfObject::string(v.as_str()));
dict.set("DV", PdfObject::string(v.as_str()));
}
if let Some(ml) = max_len {
dict.set("MaxLen", PdfObject::Integer(*ml as i64));
}
}
FieldKind::Checkbox { checked, export_value } => {
dict.set("FT", PdfObject::name("Btn"));
dict.set("T", PdfObject::string(field.name.as_str()));
dict.set("Ff", PdfObject::Integer(ff));
let val = if *checked { export_value.as_str() } else { "Off" };
dict.set("V", PdfObject::name(val));
dict.set("DV", PdfObject::name("Off"));
let mut ap_n = PdfDict::new();
ap_n.set(export_value.as_str(), PdfObject::Null);
ap_n.set("Off", PdfObject::Null);
let mut ap = PdfDict::new();
ap.set("N", PdfObject::Dictionary(ap_n));
dict.set("AP", PdfObject::Dictionary(ap));
dict.set("AS", PdfObject::name(val));
}
FieldKind::Radio { group, export_value, selected } => {
dict.set("FT", PdfObject::name("Btn"));
dict.set("T", PdfObject::string(group.as_str()));
let radio_flags = ff | (1 << 15) | (1 << 16); dict.set("Ff", PdfObject::Integer(radio_flags));
let val = if *selected { export_value.as_str() } else { "Off" };
dict.set("V", PdfObject::name(val));
dict.set("AS", PdfObject::name(val));
}
FieldKind::Dropdown { options, selected, editable } => {
dict.set("FT", PdfObject::name("Ch"));
dict.set("T", PdfObject::string(field.name.as_str()));
dict.set("DA", PdfObject::string(da.as_str()));
let mut ch_flags = ff | (1 << 17); if *editable { ch_flags |= 1 << 18; }
dict.set("Ff", PdfObject::Integer(ch_flags));
let opts: Vec<PdfObject> = options.iter()
.map(|o| PdfObject::string(o.as_str()))
.collect();
dict.set("Opt", PdfObject::Array(opts));
if let Some(ref sel) = selected {
dict.set("V", PdfObject::string(sel.as_str()));
}
}
FieldKind::ListBox { options, selected, multi_select } => {
dict.set("FT", PdfObject::name("Ch"));
dict.set("T", PdfObject::string(field.name.as_str()));
dict.set("DA", PdfObject::string(da.as_str()));
let mut lb_flags = ff;
if *multi_select { lb_flags |= 1 << 21; }
dict.set("Ff", PdfObject::Integer(lb_flags));
let opts: Vec<PdfObject> = options.iter()
.map(|o| PdfObject::string(o.as_str()))
.collect();
dict.set("Opt", PdfObject::Array(opts));
if !selected.is_empty() {
if selected.len() == 1 {
dict.set("V", PdfObject::string(selected[0].as_str()));
} else {
let sel: Vec<PdfObject> = selected.iter()
.map(|s| PdfObject::string(s.as_str()))
.collect();
dict.set("V", PdfObject::Array(sel));
}
}
}
FieldKind::Button { label } => {
dict.set("FT", PdfObject::name("Btn"));
dict.set("T", PdfObject::string(field.name.as_str()));
dict.set("Ff", PdfObject::Integer(ff | (1 << 16))); dict.set("DA", PdfObject::string(da.as_str()));
let mut mk = PdfDict::new();
mk.set("CA", PdfObject::string(label.as_str()));
dict.set("MK", PdfObject::Dictionary(mk));
}
}
if let Some(bc) = field.border_color {
let mut bs = PdfDict::new();
bs.set("Type", PdfObject::name("Border"));
bs.set("W", PdfObject::Real(field.border_width));
bs.set("S", PdfObject::name("S")); dict.set("BS", PdfObject::Dictionary(bs));
let mut mk = match dict.0.get("MK").cloned() {
Some(PdfObject::Dictionary(d)) => d,
_ => PdfDict::new(),
};
mk.set("BC", color_to_pdf_array(bc));
if let Some(bg) = field.bg_color {
mk.set("BG", color_to_pdf_array(bg));
}
dict.set("MK", PdfObject::Dictionary(mk));
}
writer.write_object(field_ref, &PdfObject::Dictionary(dict));
field_ref
}
}
fn color_to_pdf_array(c: Color) -> PdfObject {
match c {
Color::Rgb(r, g, b) => PdfObject::Array(vec![
PdfObject::Real(r), PdfObject::Real(g), PdfObject::Real(b),
]),
Color::Gray(g) => PdfObject::Array(vec![PdfObject::Real(g)]),
Color::Cmyk(c, m, y, k) => PdfObject::Array(vec![
PdfObject::Real(c), PdfObject::Real(m), PdfObject::Real(y), PdfObject::Real(k),
]),
}
}